TypeScript
The package ships hand-written typings, for TypeScript 5.0 or newer. A schema is typed by the metaschema Schema it is built from: metaschema infers the type of its values from the definition literal and publishes it in its Standard Schema props (~standard.types), and Schema.from reads them by their shape. protoarray's typings import nothing of metaschema.
import { Schema } from '@alexify/protoarray';
import { Schema as Definition } from '@alexify/metaschema';
const User = Schema.from(
new Definition('User', {
id: 'number',
name: 'string',
role: { enum: ['admin', 'editor', 'viewer'] },
address: { city: 'string', zip: '?string' },
tags: { array: 'string' },
createdAt: 'date',
email: '?string',
}),
);
const user = User.decode(wire);
// {
// id: number;
// name: string;
// role: 'admin' | 'editor' | 'viewer';
// address: { city: string; zip: string | null };
// tags: string[];
// createdAt: Date;
// email: string | null;
// }
User.encode({ id: 1, name: 'Alex', role: 'editor', address: { city: 'Kyiv' }, tags: [], createdAt: new Date() });
User.encode({ id: 'x' }); // a compile errorSchema<Output, Input> has two type parameters:
Outputis whatdecodeandparsereturn: metaschema's output type as decode gives it,Decoded<T>, with every key of a record present and the optional onesT | null(a key missing on the wire decodes asnull).Inputis whatencodeandstringifyaccept: metaschema's input type, where optional fields may be left out,nullorundefined, and a missing required field or a wrong type is a compile error.
The mapping of each form of the definition to a type is metaschema's (see its TypeScript guide): { enum: ['a', 'b'] } is 'a' | 'b', { set: T } a Set<T>, { map: { string: V } } a Map<string, V>, bytes a Uint8Array, a discriminated union the union of its branches. Decoded<T> keeps Date, Map, Set, Uint8Array and functions as they are, and makes the optional keys and tuple elements of everything else present and nullable:
import type { Decoded } from '@alexify/protoarray';
type A = Decoded<{ a: number; b?: string }>; // { a: number; b: string | null }
type T = Decoded<{ t: [number, string?] }>; // { t: [number, string | null] }A union decodes to the union of its branches, so checking the discriminator narrows it:
const Packet = Schema.from(
Definition.from({
union: [
{ Struct: {}, type: { enum: ['call'] }, id: 'string' },
{ Struct: {}, type: { enum: ['event'] }, name: 'string' },
],
discriminator: 'type',
}),
);
const packet = Packet.decode(wire);
if (packet.type === 'call') packet.id; // stringSchemas without a type
A snapshot, a definition with { metaschema } and a Model's entities carry no type the compiler can see: such a schema is Schema<unknown>. A type argument gives it one:
import { Schema, type Snapshot } from '@alexify/protoarray';
interface User {
id: number;
name: string | null;
}
const User = Schema.from<User>(snapshot as Snapshot);
User.decode(wire).name; // string | nullSchema.fromModel returns Record<string, Schema>, and Schema.from(User, options) keeps the types of User. On a client that receives its snapshots, the server's types can travel too: a module of interfaces generated by metaschema (toTypeScript) or written by hand, given as the type argument.
The rest
schema.list is typed as a Collection<Output, Input> whose decode returns Output[]. The functional entry points (encode(schema, value) and friends) are typed after the methods of the schema they get. resolve(writer, reader) is typed as the reader. The subpaths ship their own typings (registry.d.ts, resolve.d.ts, stream.d.ts, standalone.d.ts); the methods register() installs (Schema.resolve, the stream methods, schema.toSource) are declared by module augmentation, so they type-check whether register() ran or not.
| Type | Meaning |
|---|---|
Schema<Output, Input>, Collection<Output, Input> | a schema, the type of schema.list |
Decoded<T> | a value as decode gives it |
InputOf<S>, OutputOf<S> | the input and output types of a metaschema schema S |
Snapshot, ModelLike, MetaschemaModule, Typed | what a schema is built from, by shape |
SchemaOptions, ModelOptions, DecodeOptions, Layout, Backend, WireType | options and their values |
SchemaErrorCode, EncodeErrorCode, DecodeErrorCode | the code unions of the error classes |