The TypeScript type

Status: implemented

A schema has a TypeScript type twice over: statically, as Ts<typeof s>, which the type checker computes, and as text, which the runtime printer writes.

const s = { a: or(option, number), b: or(number, undefined) }
type S = Ts<typeof s>
// { readonly a?: number } & { readonly b: number | undefined }
printer()(s)
// '{readonly"a"?:number,readonly"b":undefined|number}'

Ts<> is what makes a reader's result typed: parse(s) and validate(s) return Ts<typeof s> on success, so a schema declared once is both the check and the static type, with no second declaration to drift.

The two renderings are spelled differently, and they can also differ in what they admit. The printer goes through the data form's normalization. Ts<> can't evaluate that normalization at the type level, so where it can't see a simplification it keeps a wider type. That covers a rest that is empty without being written or(), such as rest([42], [or()]), and a rest with no prefix: rest([], string) prints as readonly string[], while Ts<> gives readonly (string | undefined)[]. How RestTs renders a rest and ts/types.ts explain both. The wider type is sound for a reader's result.

Ts<typeof s>printer()(s)
computed bythe type checkera function, at run time
goes throughthe schema as writtenthe data form, toData(s)
a structrequired and optional keys as an intersectionone object type, keys sorted
a recursive schemaa recursive typethe alias name only; dataToTs also returns the type tree = … definitions

Every container is readonly; printer(true) drops the modifier. A key admitting absence is optional, a?:, and its value type leaves out undefined unless the schema admits it too — the difference exactOptionalPropertyTypes draws (absence). A bare tuple renders at its exact length, and a rest as a tail (open containers).

Examples

The demo on this page runs the Optional or undefined lesson: { a: or(option, number), b: or(number, undefined) }, whose TypeScript type marks a optional and gives b the type undefined|number. Leave b out of the value to see both readers refuse it; give it undefined to see them accept it. The lesson is defined in sample.f.mjs, and the module page's demo lists it among all the others.

Reference

Where it is implemented

Ts<> in ts/types.ts, whose README explains how it avoids TS2589; printer, dataToTs and their marked forms in ts/module.f.mjs.

Demo

The TypeScript type marks a key admitting absence with ?, and a key that must be there but may hold undefined with | undefined. Leave b out to see both readers refuse the value; give it undefined to see them accept it.

{ a: or(option, number), b: or(number, undefined) }
{} [a] → or{} [b] → or{}aboroptionnumberornumberundefined

TypeScript:

{readonly"a"?:number,readonly"b":undefined|number}

JSON Schema:

{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["b"],"additionalProperties":{"not":{}}}
Canonical form (toData)
export default [{},{"object":[{"props":{"a":{"unit":16,"number":true},"b":{"unit":2,"number":true}},"rest":{}}]}];

parse · ok

export default {"a":1,"b":2};

validate · ok

export default {"a":1,"b":2};

Emergent Testing

Idle. Press Run to start the suite.

  • ./proof.f.mjs