intertype v203.2.0
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 ofIntertypewill contain a catalog of pre-declared types ('default types'){ Intertype_minimal } = require 'intertype': instances ofIntertype_minimalwill 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'anythingis the set of all JS values;nothingis the set containingnullandundefined,somethingisanythingexceptnothing(nullandundefined).type_of xwill never test for and returnanything,nothingorsomething.nullis, unsurprisingly, the name of the valuenullandundefinedis the name of the valueundefined.unknownis the default type name returned bytype_of xwhen no other type test (except foranything,nothingandsomething) 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 nulls and undefineds. 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
createnor atemplateentry are not 'creatable'; trying to calltypes.create.〈type〉()will fail with an error. - If given, a
createentry 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 (maybenullfor all types except fornull) to indicate failure. In the latter case, anIntertype_wrong_arguments_for_createwill be thrown, assuming that the input arguments (not the create method) was at fault. Errors other thanIntertype_wrong_arguments_for_createthat are raised during calls to the create method should be considered bugs. - a type declaration with a
templatebut nocreateentry 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
objects 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 objects- 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
testentry is not a unary function - ... test method has the wrong arity
- ... when a
createentry has been given but has the wrong arity - ...
Type declarations are final, meaning that while you can use the
types.declare()method after thetypesobject has been instantiated, you cannot use it to re-declare a known type.- When instantiating
Intertypewith a series of declaration objects, any duplicate names on the objects passed in must be eliminated beforehand.
evaluate methods
- when using
isaandvalidatemethods, 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 xfails you only know that eitherxwas not an object or that any nested field such asperson.address.city.postcodewas not satisfied - prior versions of this library attempted to solve the problem by tracing the execution of all the test
triggered by calling an
isaor avalidatemethod; however, this was cumbersome and wasteful as collecting the traces needs time and RAM for each singleisaandvalidatemethod 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
evaluatemethods let users obtain a succinct catalog of all the transitive fields of a given type declaration and how they faredevaluate[type] xwill always return a flat object whose keys are fully qualified type names (likeperson.address.city); they will appear in order of their declaration withtypecoming first, so the object returned byevaluate.person xwill always havepersonas its first key, and the one returned byevaluate.person.address xwill always haveperson.addressas 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 typeboolordeeplike 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
nonemptywith its two branch typesnonempty.listandnonempty.set—we can now test for any of:isa.nonempty.list x: whetherxis alistwith non-zerox.lengthisa.nonempty.set x: whetherxis asetwith non-zerox.sizeisa.nonempty x: whetherxsatisifes eitherisa.nonempty.list xorisa.nonempty.set x
- In other words, the qualifier is what becomes the variant—
isa.nonempty.listandisa.nonempty.setare 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
emptyas aqualifierand set up tests for bothempty.textandempty.list, then the explicit variantemptycan be tested for withisa.empty x, which will returntruefor 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)templatefields: 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
fieldsentry 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: returnstrueiffxis an object with (at least) two propertiesqanduwhose individual test methods both returntrueas welltypes.isa.quantity.q x: returnstrueiffxis afloattypes.isa.quantity.u x: returnstrueiffxis atext
- at least for the time being,
- a type
Twith field declarations must have itstestentry set toobject, and, - when flat style is used, type
Tmust 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 style - field declarations constitute isolated namespaces, meaning that
types.isa.text—that is, typetextin the root namespace—is entirely separate from, say,types.isa.product.rating.text, which is typetextin theratingnamespace of typeproductin 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
Sas inT: 'some.test.here'is equivalent to using the same stringSto spell out a (possibly dotted, thus compound chain of) property accessor(s) to@isainside 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,validateandvalidate.optionalis equivalent to the bracket notation with a string literal (or a variable) on the same base; thus,isa.some.accessor xis equivalent toisa[ 'some.accessor' ] x - the
type_of()method ofIntertype_minimalinstances can only report the types ofnullandundefined(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 xandisa.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 * bBrowserify
browserify --require intertype --debug -o public/browserified/intertype.jsTo Do
- – in
_compile_declaration_object(), add validation for return value - – implement using
optionalin a declarations, as in{ foo: 'optional.text', } - – what should RHS
optional.foo.barmean, is it potentially different fromfoo.optional.bar(even if we never want to implement the latter)? Observe that wileoptional.foo.barmight mean something different thanfoo.optional.bar, when testing forisa.optional.foo.bar xwe apparently still understandfoo.baras a (compound) fully qualified name of a type (barin namespacefoo) that in its entirety may be present or absent- – consider to disallow
optionalexcept in front of a simple type name (without dots)
- – consider to disallow
- – test
create()method for the recursive case - – acquire deep-freezing method
- – what do when, in the declaration, ...
- –
evaluate.cardinal Infinityreturns{ cardinal: false };evaluate.posnaught.integer Infinityreturns{ 'posnaught.integer': false }; in both cases, more details would be elucidating (Infinitysatisfiesposnaughtbut notinteger) - – consider to enrich result of
evaluatemethods with length-limitedrpr()and type of values encountered - – implement an
explainorreportmethod that shows a table with all the tests and what the actual values were thatevaluateenountered - – work out terminology concerning fields, sub-types (as opposed to derived types), and the to-be implemented 'qualifiers'
- –
nonemptyetc. could be autogenerated: go through each enumerable propertyTofisa.nonemptyand 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:
basetypeoptionalusertypequalifiernegation(?)
- – 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
_isabut missing fromdefault_declarations - – consider to relegate private module exports to sub-key
testingor similar - – implement a type
forbiddenthat, 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
optionalcannot 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 pdetermine at what point(s) to insert a type validation; presumably, the return value of
create()(even of one generated from atemplatesetting) should be validated+ validate that
createentries are sync functions- + validate nullarity of template methods when no
createentry 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
cfgobject with an appropriate setting (default_types: true?) - by implementing
Intertype#declarationsas 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
testto the name of a declared type - + allow name-spacing a la
isa.myproject.foobar()? and use it to implementfields - + when
fieldsare implemented, also implement modified rules for test method - + in
isa.foo.bar x,foois implemented as a function with abarproperty; what about the built-in properties of functions likenameandlength?- – can we use
Function::call f, ...instead off.call ...to avoid possible difficulty ifcallshould 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
overridewith the (clearer?)replacedisallow 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
declareis called with unsuitable arguments - + unify usage, orthography of 'built ins', 'builtins' (?), 'base type(s)', 'basetype(s)' -> 'basetype(s)'
- + currently
basetypeis declared as( ( typeof x ) is 'string' ) and ( x is 'optional' or Reflect.has built_ins, x )- checking for
stringis redundant checking for( ( typeof x ) is 'string' )is not redundant as it prevents errors whenisa.basetype()is called with a non-object value - should
optionalbe included? - + fix wrong usage of
Reflect.has()in_isa.basetype()(returnstruefortoString)
- checking for
- + to fix implementation failure connected to RHS
optionalprefix: - + 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_testsacross 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
intertypeinstances 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; likeisaandvalidatemethods, 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
evaluated 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
declarationsthat 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,emptyandnonemptyare not types names of an object with fields, and the names after the dots are not field names; also,isa.nonempty xdoes 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 differentisamethod implementation- + qualifiers should be distinguished from
optionalwhich is and remains a prefix that can before any other legal (known, declared) (fully quylified) type name, soisa.optional.nonempty.text xmay be legal butisa.nonempty.optional.text xwon'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,nullincreatemethods that result in record
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago