Declarations
Types that survive to runtime: Type, Enum, Union, Generic, FunctionPredicate.
TypeScript's types vanish when the code runs. TJS declarations do not: each one is a
value, and the value is a predicate, a function you call with a candidate that
answers whether it belongs. check is that same function, not a second implementation, and
the predicate carries its facts with it: a name, a description, the example it was
declared from, and, for an Enum, the whole domain.
| Declaration | Declares | The predicate answers |
|---|---|---|
Type |
a shape, from an example | "is this shaped like the example?" |
Enum |
a named set of values | "is this one of the members?" |
Union |
an anonymous set of literals | "is this one of these?" |
Generic |
a family of types | returns a predicate per type argument |
FunctionPredicate |
a function signature | "is this a function?", and documents the contract |
The last row is the one to read twice: a FunctionPredicate describes a function's
type. It is not a predicate over values in the sense the rest of this chapter uses.
Each section below declares what it uses, so every block is a complete program. The test
blocks state what the declaration does; they run when the file is transpiled.
Type — from an example
The simplest declaration is a name and an example. The example is not a type annotation; it is a witness, a value the type must accept, and TJS infers the shape from it.
Type Name 'Alice'
test 'a Type is callable, and check is the same function' {
expect(Name('Bob')).toBe(true)
expect(Name(42)).toBe(false)
expect(Name.check === Name).toBe(true)
}
An object example declares a shape, and the block form gives the type a description:
Type User {
description: 'a user'
example: { name: '', age: 0 }
}
test 'an object example checks the shape' {
expect(User({ name: 'Ada', age: 36 })).toBe(true)
expect(User({ name: 'Ada' })).toBe(false)
expect(User.description).toBe('a user')
}
Predicates — when an example is not enough
An example says what a value looks like, not what it must satisfy. A predicate adds the
rule. In the block form, predicate { … }, the TYPE NAME stands for the value under test, so
it reads as a definition, and return is required, as in any JavaScript block:
Type Even {
example: 2
predicate { return Even % 2 === 0 }
}
Type Positive {
example: 1
predicate { return Positive > 0 }
}
test 'a predicate adds the rule the example cannot state' {
expect(Even(4)).toBe(true)
expect(Even(3)).toBe(false)
expect(Positive(-1)).toBe(false)
}
The function form, predicate(x) { … }, names the value explicitly:
Type Email {
description: 'an email address'
example: 'user@example.com'
predicate(x) {
if (typeof x !== 'string' || x.includes(' ')) return false
const at = x.indexOf('@')
return at > 0 && at === x.lastIndexOf('@') && x.lastIndexOf('.') > at + 1 && !x.endsWith('.')
}
}
test 'the function form takes the value as a parameter' {
expect(Email('ada@example.com')).toBe(true)
expect(Email('not an address')).toBe(false)
}
A predicate that is pure and synchronous is verified when it is transpiled and compiled to a fuel-bounded guard, so a hostile input cannot hang validation; it simply fails. One that cannot be verified still works, as a plain function.
A regex makes a predicate unverifiable here: a native match is one call that fuel cannot see
inside, so a pattern like /a*a*c/ could stall validation on a long input. Email above
uses string methods instead, and is verified.
A declaration serialises to its facts
Because a declaration is a function, JSON.stringify would normally drop it without a word.
It does not: a type serialises to its facts. The implementation is left out, and so is
anything else that is a function.
Type User {
description: 'a user'
example: { name: '', age: 0 }
}
test 'a type serialises to its facts, not its code' {
const facts = JSON.parse(JSON.stringify({ field: User }))
expect(facts.field.description).toBe('a user')
expect('check' in facts.field).toBe(false)
}
Enum — a named set
An Enum names its members, so the set can be referred to by name, enumerated, and
reverse-looked-up. That is also what makes it useful to an editor: the domain is right there
to offer as completions.
Enum Status 'request status' {
Pending = 'pending'
Active = 'active'
Done = 'done'
}
test 'an Enum carries its whole domain' {
expect(Status('active')).toBe(true)
expect(Status('lost')).toBe(false)
expect(Status.members.Done).toBe('done')
expect(Status.names['pending']).toBe('Pending')
expect(Status.values).toEqual(['pending', 'active', 'done'])
}
Union — an anonymous set
A Union is a closed set of literals with no names to look up. Use an Enum when the set
wants a name, a description and reverse lookup; use a Union, or a literal union written
straight into a parameter (m: 'on' | 'off'), when it does not.
Union Direction 'cardinal' 'north' | 'south' | 'east' | 'west'
test 'a Union accepts exactly its literals' {
expect(Direction('north')).toBe(true)
expect(Direction('northeast')).toBe(false)
}
Generic — a family of types
A Generic is a factory: give it a type argument and it returns a predicate. The type
parameter arrives in the predicate as a check; call it on the part it governs.
Generic Box<T> {
description: 'a boxed value'
predicate(x, T) {
return typeof x === 'object' && x !== null && 'value' in x && T(x.value)
}
}
test 'a Generic instance is itself a predicate' {
const IntBox = Box(0)
expect(IntBox({ value: 7 })).toBe(true)
expect(IntBox({ value: 'seven' })).toBe(false)
expect(IntBox(null)).toBe(false)
}
FunctionPredicate — a function's type
FunctionPredicate declares a signature: the parameters a function takes and what it
returns. At runtime the check is only "is this a function?", because whether a function
honours its signature is something you learn by calling it. The declaration's value is as a
documented contract that tools, docs and .d.ts output can read.
FunctionPredicate Formatter {
params: { input: '' }
returns: ''
}
test 'a FunctionPredicate checks callability and carries the contract' {
expect(Formatter((s) => s.toUpperCase())).toBe(true)
expect(Formatter('not a function')).toBe(false)
expect(Object.keys(Formatter.params)).toEqual(['input'])
}
One thing that differs by where the type comes from
Types declared in a .tjs file, like every one above, come from the file's own small
runtime. The same constructors imported from the tjs-lang library produce values that are
additionally instanceof Predicate and name the witness example (the inline runtime calls
it __ex). The verdicts are the same either way; see Type Identity for the full map.