A JavaScript typechecker
npm install intertypeA JavaScript type checker with helpers to implement own types and do object shape validation.
Table of Contents generated with DocToc
- InterType
- Exported Classes
- Base Types
- create.〈type〉()
- declare()
- evaluate methods
- Sum Types (Variants) and Product Types (Records)
- Declaration Values (Test Method, Type Name, Object)
- Namespaces and Object Fields
- Invariants
- Browserify
- To Do
- Is Done
* { Intertype } = require 'intertype': instances of Intertype will contain a catalog of pre-declared
types ('default types')
* { Intertype_minimal } = require 'intertype': instances of Intertype_minimal will not include the
default types
* in both cases, instances will include the basetypes
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:
``coffee`
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 containing null and undefined,something
* is anything except nothing (null and undefined).type_of x
* will never test for and return anything, nothing or something.null
* is, unsurprisingly, the name of the value null andundefined
* is the name of the value undefined.unknown
* is the default type name returned by type_of x when no other type test (except for anything,nothing
and something) returns true.
In addition to the above, the 'metatype' or 'quasitype' optional is also reserved. optional is not anull
type proper, rather, it is a type modifier to allow for optional s and undefineds. It is used inisa.optional.integer x
constructs like and validate.optional.integer x to state succinctly that 'if x isnull
given (i.e. not or undefined), it should be an integer'.
Types declarations may include a create and a template entry:
* Types that have neither a create nor a template entry are not 'creatable'; trying to calltypes.create.〈type〉()
will fail with an error.create
* If given, a entry must be a (synchronous) function that may accept any number of arguments; if ittest()
can make sense out of the values given, if any, it must return a value that passes its own null
method; otherwise, it should return any non-validating value (maybe for all types except fornull
) to indicate failure. In the latter case, an Intertype_wrong_arguments_for_create will be thrown,Intertype_wrong_arguments_for_create
assuming that the input arguments (not the create method) was at fault. Errors other than
that are raised during calls to the create method should betemplate
considered bugs.
* a type declaration with a but no create entry will become 'creatable' by being assigned antemplate
auto-generated create method.
* The auto-generated create method will accept no arguments and either
* return the value stored under , orrandom_integer
* 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 ()date
or time-dependent () values.object
* 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 s as template values
should be copied (shallow or deep, as the case may be)
* Intertype#declare() accepts any number of objectstest
* it 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 entry is not a unary functioncreate
* ... test method has the wrong arity
* ... when a entry has been given but has the wrong arity
* ...
* Type declarations are final, meaning that while you can use the types.declare() method after the typesIntertype
object has been instantiated, you cannot use it to re-declare a known type.
* When instantiating with a series of declaration objects, any duplicate names on the objects
passed in must be eliminated beforehand.
methods* when using isa and validate methods, it can be difficult to see exactly what went wrong when a testisa.employee_record x
fails
* this is all the more true with nesting types that have complex fields as properties of complex fields;
when fails you only know that either x was not an object or that any nestedperson.address.city.postcode
field such as was not satisfiedisa
* prior versions of this library attempted to solve the problem by tracing the execution of all the test
triggered by calling an or a validate method; however, this was cumbersome and wasteful asisa
collecting the traces needs time and RAM for each single and validate method call, whether theevaluate
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
* methods let users obtain a succinct catalog of all the transitive fields of a given typeevaluate[type] x
declaration and how they fared
* will always return a flat object whose keys are fully qualified type names (likeperson.address.city
); they will appear in order of their declaration with type coming first, so theevaluate.person x
object returned by will always have person as its first key, and the one returned byevaluate.person.address x
will always have person.address as its first key
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 type boolordeep like this:
`coffee`
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:
`coffee`
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 types nonempty.list andnonempty.set
—we can now test for any of:isa.nonempty.list x
* : whether x is a list with non-zero x.lengthisa.nonempty.set x
* : whether x is a set with non-zero x.sizeisa.nonempty x
* : whether x satisifes either isa.nonempty.list x or isa.nonempty.set xisa.nonempty.list
* In other words, the qualifier is what becomes the variant— and isa.nonempty.set arefoo: (
ordinary tpes (though conceivably one could declare them as unqualified variants, as shown above)
* '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 (
x ) -> ( @isa.a x ) or ( @isa.b x ))empty
an explicit variant* comes about by using a (chain of) qualifier(s) as a typename; so when we have
declared as a qualifier and set up tests for both empty.text and empty.list, then theempty
explicit variant can be tested for with isa.empty x, which will return true for exactly the''
values and [].
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 declarations
* create()
* 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: returns true iff x is an object with (at least) two properties q and utrue
whose individual test methods both return as welltypes.isa.quantity.q x
* : returns true iff x is a floattypes.isa.quantity.u x
* : returns true iff x is a textT
* at least for the time being,
* a type with field declarations must have its test entry set to object, and,T
* when flat style is used, type must be declared before any field is declared.T
* if there is an existing declaration for type , the only way to add fields to it is by using the flattypes.isa.text
declaration style
* field declarations constitute isolated namespaces, meaning that —that is, type text intypes.isa.product.rating.text
the root namespace—is entirely separate from, say, , which is type textrating
in the namespace of type product 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', }
* a declaration that identifies a known type with a string of characters S as in T: 'some.test.here' isS
equivalent to using the same string to spell out a (possibly dotted, thus compound chain of) property@isa
accessor(s) to inside a test method, as in T: ( x ) -> @isa.some.test.here xisa
* a constant (literal) property accessor (which may be dotted or not) to , isa.optional, validatevalidate.optional
and is equivalent to the bracket notation with a string literal (or a variable) on theisa.some.accessor x
same base; thus, is equivalent to isa[ 'some.accessor' ] xtype_of()
* the method of Intertype_minimal instances can only report the types of null andundefined
(as 'null' and undefined'); all other values are considered 'unknown'. However, it isisa.anything x
possible to test for , isa.nothing x, isa.something x, isa.unknown x (and, ofisa.undefined x
course, and isa.null x).
partial / incomplete type system
total type system
(may fail)
`coffee`
mulint: ( a, b = 1 ) ->
validate.integer a
validate.integer b
return validate.integer a * b
`bash`
browserify --require intertype --debug -o public/browserified/intertype.js
* [–] in _compile_declaration_object(), add validation for return valueoptional
* [–] implement using in a declarations, as in { foo: 'optional.text', }optional.foo.bar
* [–] what should RHS mean, is it potentially different from foo.optional.bar (evenoptional.foo.bar
if we never want to implement the latter)? Observe that wile might mean somethingfoo.optional.bar
different than , when testing for isa.optional.foo.bar x we apparently stillfoo.bar
understand as a (compound) fully qualified name of a type (bar in namespace foo) that in itsoptional
entirety may be present or absent
* [–] consider to disallow except in front of a simple type name (without dots)create()
* [–] test method for the recursive casefields
* [–] 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 but no templatetemplate
* [–] there's but no fieldstemplate
* [–] there's but no fields, and all fields in template are constants (is it even worthtemplate
caring about?)
* [–] there's but it's a functionevaluate.cardinal Infinity
* [–] returns { cardinal: false }; evaluate.posnaught.integer Infinity{ 'posnaught.integer': false }
returns ; in both cases, more details would be elucidating (Infinityposnaught
satisfies but not integer)evaluate
* [–] consider to enrich result of methods with length-limited rpr() and type of valuesexplain
encountered
* [–] implement an or report method that shows a table with all the tests and what theevaluate
actual values were that enounterednonempty
* [–] work out terminology concerning fields, sub-types (as opposed to derived types), and the to-be
implemented 'qualifiers'
* [–] etc. could be autogenerated: go through each enumerable property T ofisa.nonempty
and add a test ( @isa[ T ] x ) and ( @isa.nonempty[ T ] xisa.quantity()
* [–] need a term for the 'sub-methods' that get attached as props to the 'target methods'(??), e.g.
after has been set 'sub-methods' isa.quantity.q(), isa.quantity.u() will be set asisa.quantity
properties of their 'target' ; the current terminology is unfortunate and obfuscates moreoptional
than it elicits
* [–] clarify difference between basetypes and meta/quasitype , provide a type for the unionbasetype
of both
* [–] Type roles:
* optional
* usertype
* qualifier
* negation
* (?)role
* [–] in addition to setting , allow users to set { test: '@qualifier' } which then can beempty: '@qualifier'
shortened to in dotted field enumerations (maybe really the preferred way to_isa
differentiate qualifier trees from nested records)
* [–] implement method to supply all types that are present in but missing fromdefault_declarations
testing
* [–] consider to relegate private module exports to sub-key or similarforbidden
* [–] implement a type 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
* [+] hard-wire basic types anything, nothing, something, null, undefined, unknown{ type_of } = new Intertype()
* [+] allow stand-alone methods ()Validation_error
* [+] ensure all methods have reasonable names
* [+] use proper error types like type_of()
* [+] 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 is called with wrong arityisa.quux x
* [+] throw error with instructive message when an undefined type is being accessed as in optional
* [+] ensure that cannot be used as a type name
* [+] type-check declaration function (a.k.a. isa-test)
* [+] given a declaration like this:
`coffee`
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() (eventemplate
of one generated from a setting) should be validated
* [+] validate that create entries are sync functionscreate
* [+] validate nullarity of template methods when no entry is presentdeclare()
* [+] implement a way to keep standard declarations and add own ones on top:
* by implementing a method (which accepts an object with named declarations)default_declarations
* by exporting (a copy of) cfg
* by allowing or requiring a object with an appropriate setting (default_types: true?)Intertype#declarations
* by implementing as a class with an add() method or similarbuilt_ins
* [+] allow overrides when so configured but not of ? the 'basetypes'anything
, nothing, something, null, undefined, unknown, or the 'meta type' optionaltest
* [+] what about declarations with missing ? ensure an error is thrown when no testtest
method is present
* [+] enable setting to the name of a declared typeisa.myproject.foobar()
* [+] allow name-spacing a la fields? and use it to implement fields
* [+] when are implemented, also implement modified rules for test methodisa.foo.bar x
* [+] in , foo is implemented as a function with a bar property; what about thename
built-in properties of functions like and length?Function::call f, ...
* [–] can we use instead of f.call ... to avoid possible difficulty ifcall
should get shadowed?_Intertype
* [+] allow declaration objects
* [+] remove 'dogfeeding' (class ), directly use test methods from catalog insteadget_isa()
* [+] fix failure to call sub-tests for dotted type references
* [+] fix failure to validate dotted type
* [+] make &c privateoverride
* [+] consider to replace with the (clearer?) replace disallowdeclare()
overrides
* [+] remove indirection of , _declare() keep indirection of declare() todeclare
avoid 'JavaScript Rip-Off' effect when detaching unbound method
* [+] test whether correct error is thrown throw meaningful error when basetype
is called with unsuitable arguments
* [+] unify usage, orthography of 'built ins', 'builtins' (?), 'base type(s)', 'basetype(s)' ->
'basetype(s)'
* [+] currently is declared as ( ( typeof x ) is 'string' ) and ( x is 'optional' or
Reflect.has built_ins, x )string
checking for is redundant checking for ( ( typeof x ) is 'string' ) is not*isa.basetype()
redundant as it prevents errors when is called with a non-object valueoptional
* should be included?Reflect.has()
* [+] fix wrong usage of in _isa.basetype() (returns true for toString)optional
* [+] to fix implementation failure connected to RHS prefix:is_optional
* [+] commit current state, mistakes and all
* [+] identify and rip out all places concerned with and/or RHS optional prefixoptional
* [+] reduce tests such that valuable tests are preserved but ones using RHS prefix areIntertype_minimal
skipped
* [+] whatever the outcome, update docs
* [+] test whether basic types are immutable with instances of _compile_declaration_object()
* [+] in , call recursively for each entry in declaration.fieldssub_tests
* [+] find a way to avoid code duplication in handling of field across all four test methodsisa
(, isa.optional, validate, validate.optional) declarations[ type ].test(); can we bake those right into
? But then what when more fields get declared?intertype
* this wouldn't pose a problem if we required that instances be closed for furthercreate()
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
* [+] implement value creation for all the builtin types
* [+] when fields are declared but no method is given, generate a create() method thatObject.assign()
accepts any number of objects that, together with the template, will be condensed into one object using
merge()
* [+] test that template functions are called, even when used in template fields
* [+] we should use a recursive method, call it deepmerge(), instead of Object.assign()create()
when creating values from templates; this method should be exported for the benefit of users who want to
implement their own method; conceivably, deepmerge() could / should beimplemented inwebguy.props
evaluate.[typename] x
* [+] implement method ; like isa and validate methods, however does notevaluate
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 d value is null, do we want the full complement of all the type's sub-fields{ [type]: false, }
in the result or is it better to just return ?declarations
* [+] implement a generated field in that eumerates all fully qualified field names thatwalk_transitive_field_names()
belong to the type in question; field generated by module-level method { test: 'object', }
* [+] 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 fields
* [+] implement nonempty.text()
* [+] test that incorrect templates are rejected
* [+] consider to implement , nonempty.list(), empty.text(), empty.list(); here,empty
and nonempty are not types names of an object with fields, and the names after the dots are notisa.nonempty x
field names; also, does not necessarily have to make sense so either it shouldn't be anonempty.text
* [+] implement 'qualifiers' (as in ) that look a lot like object fields but have aisa
different method implementationoptional
* [+] qualifiers should be distinguished from which is and remains a prefix that can beforeisa.optional.nonempty.text x
any other legal (known, declared) (fully quylified) type name, so may beisa.nonempty.optional.text x
legal but won'tthrows()
function or, if it is one, it should throw an error when called, something that has always been ruled out
so far
* [+] use prototypes of test methods &c for new version of guy-testequals()
* [+] use prototype of set equality for implementation in webguyundefined
* [+] allow , null in create` methods that result in record