intertype
v203.2.0
Published
A JavaScript typechecker
Downloads
175
Readme
InterType
A JavaScript type checker with helpers to implement own types and do object shape validation.
Table of Contents generated with DocToc
InterType
Exported Classes
{ Intertype } = require 'intertype'
: instances ofIntertype
will contain a catalog of pre-declared types ('default types'){ Intertype_minimal } = require 'intertype'
: instances ofIntertype_minimal
will not include the default types- in both cases, instances will include the basetypes
Base Types
The following basetypes are built-in and treated specially; they are always present and cannot be overwritten or omitted. The definitions of their test methods reads like pseudo-code:
anything: ( x ) -> true
nothing: ( x ) -> not x?
something: ( x ) -> x?
null: ( x ) -> x is null
undefined: ( x ) -> x is undefined
unknown: ( x ) -> ( @type_of x ) is 'unknown'
anything
is the set of all JS values;nothing
is the set containingnull
andundefined
,something
isanything
exceptnothing
(null
andundefined
).type_of x
will never test for and returnanything
,nothing
orsomething
.null
is, unsurprisingly, the name of the valuenull
andundefined
is the name of the valueundefined
.unknown
is the default type name returned bytype_of x
when no other type test (except foranything
,nothing
andsomething
) returnstrue
.
In addition to the above, the 'metatype' or 'quasitype' optional
is also reserved. optional
is not a
type proper, rather, it is a type modifier to allow for optional null
s and undefined
s. It is used in
constructs like isa.optional.integer x
and validate.optional.integer x
to state succinctly that 'if x is
given (i.e. not null
or undefined
), it should be an integer'.
create.〈type〉()
Types declarations may include a create
and a template
entry:
- Types that have neither a
create
nor atemplate
entry are not 'creatable'; trying to calltypes.create.〈type〉()
will fail with an error. - If given, a
create
entry must be a (synchronous) function that may accept any number of arguments; if it can make sense out of the values given, if any, it must return a value that passes its owntest()
method; otherwise, it should return any non-validating value (maybenull
for all types except fornull
) to indicate failure. In the latter case, anIntertype_wrong_arguments_for_create
will be thrown, assuming that the input arguments (not the create method) was at fault. Errors other thanIntertype_wrong_arguments_for_create
that are raised during calls to the create method should be considered bugs. - a type declaration with a
template
but nocreate
entry will become 'creatable' by being assigned an auto-generated create method. - The auto-generated create method will accept no arguments and either
- return the value stored under
template
, or - call the template method, if it is a synchronous function; this is not only how one can have a function
being returned by an auto-generated create method, this is also a way to produce new copies instead of
always returning the identical same object, and, furthermore, a way to return random (
random_integer
) or time-dependent (date
) values. - anything else but a synchronous function (primitive values, but also asynchronous functions) will just
be returned as-is from the auto-generated create method
- but this behavior may be slightly modified in the future, especially
object
s as template values should be copied (shallow or deep, as the case may be)
- but this behavior may be slightly modified in the future, especially
- return the value stored under
declare()
Intertype#declare()
accepts any number of objectsit will iterate over all key, value pairs and interpret
- the key as the type (name), and
- the value as either that type's test method, or, if it's an object, as a type declaration
the declaration will be rejected if the type name...
- ... is one of the basetypes, or
- ... is already declared
the declaration will be rejected if the declaration ...
- ... is missing a test method
- ... when the
test
entry is not a unary function - ... test method has the wrong arity
- ... when a
create
entry has been given but has the wrong arity - ...
Type declarations are final, meaning that while you can use the
types.declare()
method after thetypes
object has been instantiated, you cannot use it to re-declare a known type.When instantiating
Intertype
with a series of declaration objects, any duplicate names on the objects passed in must be eliminated beforehand.
evaluate
methods
- when using
isa
andvalidate
methods, it can be difficult to see exactly what went wrong when a test fails - this is all the more true with nesting types that have complex fields as properties of complex fields;
when
isa.employee_record x
fails you only know that eitherx
was not an object or that any nested field such asperson.address.city.postcode
was not satisfied - prior versions of this library attempted to solve the problem by tracing the execution of all the test
triggered by calling an
isa
or avalidate
method; however, this was cumbersome and wasteful as collecting the traces needs time and RAM for each singleisa
andvalidate
method call, whether the traces are used afterwards or, most of the time, silently discarded - another problem with tracing is that, in the interest of performance, tests are shortcut, meaning that the first failed test in a series of tests will cause a negative result, without the subsequent tests being performed; this means that traces can only ever report the first failure of a complex type check, not all of the failures
evaluate
methods let users obtain a succinct catalog of all the transitive fields of a given type declaration and how they faredevaluate[type] x
will always return a flat object whose keys are fully qualified type names (likeperson.address.city
); they will appear in order of their declaration withtype
coming first, so the object returned byevaluate.person x
will always haveperson
as its first key, and the one returned byevaluate.person.address x
will always haveperson.address
as its first key
Sum Types (Variants) and Product Types (Records)
Variants can be defined with or without a qualifier, a syntactic element that precedes a type name
Variants without qualifiers ('unqualified variants') can be defined by using a logical disjunction (
or
,||
) in the test method. For example, one could declare a typeboolordeep
like this:declarations: boolordeep: ( x ) -> ( @isa.boolean x ) or ( x is 'deep' )
which will be satisfied by any one of the three values
true
,false
, and (the string)'deep'
, to the exclusion of any other values.'Qualified variants' do use an explicit qualifier that is meant to be used in conjunction with other types, for example:
declarations: nonempty: { role: 'qualifier', } 'nonempty.list': ( x ) -> ( @isa.list x ) and ( x.length isnt 0 ) 'nonempty.set': ( x ) -> ( @isa.set x ) and ( x.size isnt 0 )
- Having declared the above types—the qualifier
nonempty
with its two branch typesnonempty.list
andnonempty.set
—we can now test for any of:isa.nonempty.list x
: whetherx
is alist
with non-zerox.length
isa.nonempty.set x
: whetherx
is aset
with non-zerox.size
isa.nonempty x
: whetherx
satisifes eitherisa.nonempty.list x
orisa.nonempty.set x
- In other words, the qualifier is what becomes the variant—
isa.nonempty.list
andisa.nonempty.set
are ordinary tpes (though conceivably one could declare them as unqualified variants, as shown above)
- Having declared the above types—the qualifier
'Product types' or 'records', on the other hand, are types that mandate the presence not of alternatives, but of named fields each of which must satisfy its own test method for the record to be valid
Clarification of terminology:
- an explicit qualifier is what goes in front of a qualified typename;
- a variant is a type that has several equivalent alternatives to choose from;
- an implicit variant is a type that has several equivalent alternatives to choose from that are however
not formally declared to the InterType API but inside a test method that contains disjunctions (
foo: ( x ) -> ( @isa.a x ) or ( @isa.b x )
) - an explicit variant comes about by using a (chain of) qualifier(s) as a typename; so when we have
declared
empty
as aqualifier
and set up tests for bothempty.text
andempty.list
, then the explicit variantempty
can be tested for withisa.empty x
, which will returntrue
for exactly the values''
and[]
.
Declaration Values (Test Method, Type Name, Object)
A valid declaration is either
- a test method, or
- the name of an existing type, or
- an object with the following fields:
test
: either a test method or the name of existing type (the latter will be compiled into the former)template
fields
: keys are type names, values are declarationscreate()
Namespaces and Object Fields
two ways to specify fields on objects
either in the 'nested style', by using the
fields
entry of a type declaration; for example:declarations: quantity: test: 'object' fields: q: 'float' u: 'text'
or in the 'flat style', by using dot notation in the type name:
declarations: quantity: 'object' 'quantity.q': 'float' 'quantity.u': 'text'
the two styles are identical and have the same result.
you can now call the following test methods:
types.isa.quantity x
: returnstrue
iffx
is an object with (at least) two propertiesq
andu
whose individual test methods both returntrue
as welltypes.isa.quantity.q x
: returnstrue
iffx
is afloat
types.isa.quantity.u x
: returnstrue
iffx
is atext
at least for the time being,
- a type
T
with field declarations must have itstest
entry set toobject
, and, - when flat style is used, type
T
must be declared before any field is declared.
- a type
if there is an existing declaration for type
T
, the only way to add fields to it is by using the flat declaration stylefield declarations constitute isolated namespaces, meaning that
types.isa.text
—that is, typetext
in the root namespace—is entirely separate from, say,types.isa.product.rating.text
, which is typetext
in therating
namespace of typeproduct
in the root namespace.fields can be indefinitely nested, e.g.:
types.declare { 'person': 'object', } types.declare { 'person.name': 'text', } types.declare { 'person.address': 'object', } types.declare { 'person.address.city': 'object', } types.declare { 'person.address.city.name': 'text', } types.declare { 'person.address.city.postcode': 'text', }
Invariants
- a declaration that identifies a known type with a string of characters
S
as inT: 'some.test.here'
is equivalent to using the same stringS
to spell out a (possibly dotted, thus compound chain of) property accessor(s) to@isa
inside a test method, as inT: ( x ) -> @isa.some.test.here x
- a constant (literal) property accessor (which may be dotted or not) to
isa
,isa.optional
,validate
andvalidate.optional
is equivalent to the bracket notation with a string literal (or a variable) on the same base; thus,isa.some.accessor x
is equivalent toisa[ 'some.accessor' ] x
- the
type_of()
method ofIntertype_minimal
instances can only report the types ofnull
andundefined
(as'null'
andundefined'
); all other values are considered'unknown'
. However, it is possible to test forisa.anything x
,isa.nothing x
,isa.something x
,isa.unknown x
(and, of course,isa.undefined x
andisa.null x
).
partial / incomplete type system total type system
(may fail)
mulint: ( a, b = 1 ) ->
validate.integer a
validate.integer b
return validate.integer a * b
Browserify
browserify --require intertype --debug -o public/browserified/intertype.js
To Do
- [–] in
_compile_declaration_object()
, add validation for return value - [–] implement using
optional
in a declarations, as in{ foo: 'optional.text', }
- [–] what should RHS
optional.foo.bar
mean, is it potentially different fromfoo.optional.bar
(even if we never want to implement the latter)? Observe that wileoptional.foo.bar
might mean something different thanfoo.optional.bar
, when testing forisa.optional.foo.bar x
we apparently still understandfoo.bar
as a (compound) fully qualified name of a type (bar
in namespacefoo
) that in its entirety may be present or absent- [–] consider to disallow
optional
except in front of a simple type name (without dots)
- [–] consider to disallow
- [–] test
create()
method for the recursive case - [–] acquire deep-freezing method
- [–] use deep-freezing for declaration
- [–] use deep-freezing for generated values when so configured / by default? maybe instantiation setting?
- [–] what do when, in the declaration, ...
- [–] there's
fields
but notemplate
- [–] there's
template
but nofields
- [–] there's
template
but nofields
, and all fields intemplate
are constants (is it even worth caring about?) - [–] there's
template
but it's a function
- [–] there's
- [–]
evaluate.cardinal Infinity
returns{ cardinal: false }
;evaluate.posnaught.integer Infinity
returns{ 'posnaught.integer': false }
; in both cases, more details would be elucidating (Infinity
satisfiesposnaught
but notinteger
) - [–] consider to enrich result of
evaluate
methods with length-limitedrpr()
and type of values encountered - [–] implement an
explain
orreport
method that shows a table with all the tests and what the actual values were thatevaluate
enountered - [–] work out terminology concerning fields, sub-types (as opposed to derived types), and the to-be implemented 'qualifiers'
- [–]
nonempty
etc. could be autogenerated: go through each enumerable propertyT
ofisa.nonempty
and add a test( @isa[ T ] x ) and ( @isa.nonempty[ T ] x
- [–] need a term for the 'sub-methods' that get attached as props to the 'target methods'(??), e.g.
after
isa.quantity()
has been set 'sub-methods'isa.quantity.q()
,isa.quantity.u()
will be set as properties of their 'target'isa.quantity
; the current terminology is unfortunate and obfuscates more than it elicits - [–] clarify difference between basetypes and meta/quasitype
optional
, provide a type for the union of both - [–] Type roles:
basetype
optional
usertype
qualifier
negation
(?)
- [–] in addition to setting
role
, allow users to set{ test: '@qualifier' }
which then can be shortened toempty: '@qualifier'
in dotted field enumerations (maybe really the preferred way to differentiate qualifier trees from nested records) - [–] implement method to supply all types that are present in
_isa
but missing fromdefault_declarations
- [–] consider to relegate private module exports to sub-key
testing
or similar - [–] implement a type
forbidden
that, in contradistinction to established rules, does throw an error from its test method when a record with a field thusly marked is encountered; this is to ease transition when extransous fields are removed from record types
Is Done
[+] hard-wire basic types
anything
,nothing
,something
,null
,undefined
,unknown
[+] allow stand-alone methods (
{ type_of } = new Intertype()
)[+] ensure all methods have reasonable names
[+] use proper error types like
Validation_error
[+] make it possible for Intertype methods to use an internal, private instance so type and arity testing is possible for its own methods
[+] throw error with instructive message when a type testing or
type_of()
is called with wrong arity[+] throw error with instructive message when an undefined type is being accessed as in
isa.quux x
[+] ensure that
optional
cannot be used as a type name[+] type-check declaration function (a.k.a. isa-test)
[+] given a declaration like this:
declarations = float: test: ( x ) -> Number.isFinite x create: ( p ) -> parseFloat p
determine at what point(s) to insert a type validation; presumably, the return value of
create()
(even of one generated from atemplate
setting) should be validated[+] validate that
create
entries are sync functions[+] validate nullarity of template methods when no
create
entry is present[+] implement a way to keep standard declarations and add own ones on top:
- by implementing a
declare()
method (which accepts an object with named declarations) - by exporting (a copy of)
default_declarations
- by allowing or requiring a
cfg
object with an appropriate setting (default_types: true
?) - by implementing
Intertype#declarations
as a class with anadd()
method or similar
- by implementing a
[+] allow overrides when so configured but not of
built_ins
? the 'basetypes'anything
,nothing
,something
,null
,undefined
,unknown
, or the 'meta type'optional
[+] what about declarations with missing
test
? ensure an error is thrown when no test method is present[+] enable setting
test
to the name of a declared type[+] allow name-spacing a la
isa.myproject.foobar()
? and use it to implementfields
[+] when
fields
are implemented, also implement modified rules for test method[+] in
isa.foo.bar x
,foo
is implemented as a function with abar
property; what about the built-in properties of functions likename
andlength
?- [–] can we use
Function::call f, ...
instead off.call ...
to avoid possible difficulty ifcall
should get shadowed?
- [–] can we use
[+] allow declaration objects
[+] remove 'dogfeeding' (class
_Intertype
), directly use test methods from catalog instead[+] fix failure to call sub-tests for dotted type references
[+] fix failure to validate dotted type
[+] make
get_isa()
&c private[+] consider to replace
override
with the (clearer?)replace
disallow overrides[+] remove indirection of
declare()
,_declare()
keep indirection ofdeclare()
to avoid 'JavaScript Rip-Off' effect when detaching unbound method[+] test whether correct error is thrown throw meaningful error when
declare
is called with unsuitable arguments[+] unify usage, orthography of 'built ins', 'builtins' (?), 'base type(s)', 'basetype(s)' -> 'basetype(s)'
[+] currently
basetype
is declared as( ( typeof x ) is 'string' ) and ( x is 'optional' or Reflect.has built_ins, x )
- checking for
string
is redundant checking for( ( typeof x ) is 'string' )
is not redundant as it prevents errors whenisa.basetype()
is called with a non-object value - should
optional
be included? - [+] fix wrong usage of
Reflect.has()
in_isa.basetype()
(returnstrue
fortoString
)
- checking for
[+] to fix implementation failure connected to RHS
optional
prefix:- [+] commit current state, mistakes and all
- [+] identify and rip out all places concerned with
is_optional
and/or RHSoptional
prefix - [+] reduce tests such that valuable tests are preserved but ones using RHS
optional
prefix are skipped - [+] whatever the outcome, update docs
[+] test whether basic types are immutable with instances of
Intertype_minimal
[+] in
_compile_declaration_object()
, call recursively for each entry indeclaration.fields
[+] find a way to avoid code duplication in handling of field
sub_tests
across all four test methods (isa
,isa.optional
,validate
,validate.optional
) ; can we bake those right intodeclarations[ type ].test()
? But then what when more fields get declared?- this wouldn't pose a problem if we required that
intertype
instances be closed for further declarations before being used first; this could happen implicitly on first use - if we didn't want that, we'd have to re-formulate the declaration's test method each time a field is declared for a given type
- this wouldn't pose a problem if we required that
[+] implement value creation for all the builtin types
[+] when fields are declared but no
create()
method is given, generate acreate()
method that accepts any number of objects that, together with the template, will be condensed into one object usingObject.assign()
[+] test that template functions are called, even when used in template fields
[+] we should use a recursive
merge()
method, call itdeepmerge()
, instead ofObject.assign()
when creating values from templates; this method should be exported for the benefit of users who want to implement their owncreate()
method; conceivably,deepmerge()
could / should beimplemented inwebguy.props
[+] implement method
evaluate.[typename] x
; likeisa
andvalidate
methods, however does not shortcut on failure but runs through all tests, returns object with named results so one can see e.g. which fields did and which ones didn't conform- [+] if an
evaluate
d value isnull
, do we want the full complement of all the type's sub-fields in the result or is it better to just return{ [type]: false, }
?
- [+] if an
[+] implement a generated field in
declarations
that eumerates all fully qualified field names that belong to the type in question; field generated by module-level methodwalk_transitive_field_names()
[+] would it be worth the effort to try and implement a 'permanent debugging' facility, one whose calls are left in the code (maybe in the form of specially formatted comments) and can be activated when needed? One could imagine those to produce a complete trace when activated that goes into an SQLite DB and can then be inspected and filtered as needed. This would obviously be outside the scope of the present package
[+] test that a declaration with fields defaults to
{ test: 'object', }
[+] implement
fields
[+] test that incorrect templates are rejected
[+] consider to implement
nonempty.text()
,nonempty.list()
,empty.text()
,empty.list()
; here,empty
andnonempty
are not types names of an object with fields, and the names after the dots are not field names; also,isa.nonempty x
does not necessarily have to make sense so either it shouldn't be a[+] implement 'qualifiers' (as in
nonempty.text
) that look a lot like object fields but have a differentisa
method implementation- [+] qualifiers should be distinguished from
optional
which is and remains a prefix that can before any other legal (known, declared) (fully quylified) type name, soisa.optional.nonempty.text x
may be legal butisa.nonempty.optional.text x
won't function or, if it is one, it should throw an error when called, something that has always been ruled out so far
- [+] qualifiers should be distinguished from
[+] use prototypes of test methods
throws()
&c for new version ofguy-test
[+] use prototype of set equality for
equals()
implementation inwebguy
[+] allow
undefined
,null
increate
methods that result in record