Typecraft is a library for performing type-level magic. For example, you can use it to safely typecast values.
import type { Type } from "typecraft";
import { string, number, boolean, object, optional, cast } from "typecraft";
interface Person {
name: string;
age: number;
alive?: boolean | undefined;
}
// Craft a new person type.
const person: Type<Person> = object({
name: string,
age: number,
alive: optional(boolean)
});
/**
* Create a new magic spell, a.k.a. function,
* to typecast unknown values to person type.
*/
const personify = cast(person);
const fetchPerson = async (url: string): Promise<Person> => {
const response = await fetch(url);
const data: unknown = await response.json();
const result = personify(data); // Cast the personify spell.
switch (result.status) {
case "success":
return result.value; // The result contains the person value.
case "failure":
console.error(result); // The result has debugging information.
throw new TypeError("Could not parse the response data.");
// no default
}
};
Typecasting a value produces an entirely new value. It follows the principle of "parse, don't validate". Hence, typecasting is more powerful than simple data validation. For example, because Type
is functorial you can transfigure a value while typecasting it using the map
function. This is very useful when you get data from an API in one format, but you want to transform it into another format for ease of use.
import type { Type } from "typecraft";
import { string, number, boolean, object, optional, map } from "typecraft";
interface Person {
name: string;
age: number;
alive: boolean; // The alive property is required.
}
const person: Type<Person> = map(
// Provide a default value for alive.
({ name, age, alive = true }) => ({
name,
age,
alive
}),
object({
name: string,
age: number,
alive: optional(boolean)
})
);
import type { Type } from "typecraft";
import { string, array } from "typecraft";
const strings: Type<string> = array(string);
Type<A>
is a description of A
. Think of it as a recipe to create a value of type A
. It's the main data type of typecraft. All type combinators return a Type
.
import type { Typedef } from "typecraft";
import { string, array } from "typecraft";
// type Strings = string[];
type Strings = Typedef<typeof strings>;
const strings = array(string);
Typedef<Type<A>>
returns the type A
. This is useful when you want to get the type described by a type combinator.
declare const unknown: Type<unknown>;
import { unknown } from "typecraft";
The unknown
type combinator describes unknown values. Typecasting unknown
returns a function which always succeeds. The resultant value is the same as the input.
declare const never: Type<never>;
import { never } from "typecraft";
The never
type combinator describes values which don't exist. Typecasting never
returns a functions which always fails.
declare const string: Type<string>;
import { string } from "typecraft";
The string
type combinator describes strings. Typecasting string
returns a function which only succeeds if the input is a string.
declare const number: Type<number>;
import { number } from "typecraft";
The number
type combinator describes numbers. Typecasting number
returns a function which only succeeds if the input is a number.
declare const bigint: Type<bigint>;
import { bigint } from "typecraft";
The bigint
type combinator describes bigints. Typecasting bigint
returns a function which only succeeds if the input is a bigint.
declare const boolean: Type<boolean>;
import { boolean } from "typecraft";
The boolean
type combinator describes booleans. Typecasting boolean
returns a function which only succeeds if the input is a boolean.
declare const symbol: Type<symbol>;
import { symbol } from "typecraft";
The symbol
type combinator describes symbols. Typecasting symbol
returns a function which only succeeds if the input is a symbol.
declare interface Primitives {
string: string;
number: number;
bigint: bigint;
boolean: boolean;
symbol: symbol;
null: null;
undefined: undefined;
}
declare const primitive: <A extends keyof Primitives>(
type: A
) => Type<Primitives[A]>;
import type { Type } from "typecraft";
import { primitive } from "typecraft";
const nil: Type<null> = primitive("null");
const undef: Type<undefined> = primitive("undefined");
The primitive
type combinator describes primitive values of a certain type. For example, primitive("string")
describes strings. Usually, you'd want to use one of the other combinators like string
.
declare const array: <A>(type: Type<A>) => Type<A[]>;
import { array } from "typecraft";
The array
type combinator describes arrays. It takes a single type combinator, describing items of the array, as an input.
declare type Types<A> = {
[T in keyof A]: Type<A[T]>;
};
declare const tuple: <A extends unknown[]>(...types: Types<A>) => Type<A>;
import { tuple } from "typecraft";
The tuple
type combinator describes tuples. It takes zero or more type combinators, describing items of the tuple, as an input.
declare const record: <A>(type: Type<A>) => Type<Record<PropertyKey, A>>;
import { record } from "typecraft";
The record
type combinator describes records. It takes a single type combinator, describing values of the record, as an input.
declare type Types<A> = {
[T in keyof A]: Type<A[T]>;
};
declare const object: <A extends {}>(propTypes: Types<A>) => Type<A>;
import { object } from "typecraft";
The object
type combinator describes objects. It takes a single object whose values are type combinators as an input. Typecasting object
returns a function which only succeeds if the input is an object with the shape of the object description provided.
declare const nullable: <A>(type: Type<A>) => Type<A | null>;
import { nullable } from "typecraft";
The nullable
type combinator describes nullable types. It takes a single type combinator as an input.
declare const optional: <A>(type: Type<A>) => Type<A | undefined>;
import { optional } from "typecraft";
The optional
type combinator describes optional types. It takes a single type combinator as an input.
declare type Primitive =
| string
| number
| bigint
| boolean
| symbol
| null
| undefined;
declare const enumeration: <A extends Primitive[]>(
...values: A
) => Type<A[number]>;
import type { Typedef } from "typecraft";
import { enumeration } from "typecraft";
// type Gender = "male" | "female";
type Gender = Typedef<typeof gender>;
const gender = enumeration("male", "female");
The enumeration
type combinator describes an enum of primitive values. It takes zero or more primitive values as an input.
declare type Types<A> = {
[T in keyof A]: Type<A[T]>;
};
declare const union: <A extends unknown[]>(
...types: Types<A>
) => Type<A[number]>;
import type { Primitive, Type } from "typecraft";
import {
string,
number,
bigint,
boolean,
symbol,
primitive,
union
} from "typecraft";
const simple: Type<Primitive> = union(
string,
number,
bigint,
boolean,
symbol,
primitive("null"),
primitive("undefined")
);
The union
type combinator describes a union of multiple types. It take one or more type combinators as an input.
declare type Types<A> = {
[T in keyof A]: Type<A[T]>;
};
declare const intersection: <A extends unknown[]>(
...types: Types<A>
) => Type<A>;
import type { Type } from "typecraft";
import { string, number, object, intersection } from "typecraft";
interface Foo {
foo: string;
}
interface Bar {
bar: string;
}
const foobar: Type<[Foo, Bar]> = intersection(
object({ foo: string }),
object({ bar: number })
);
The intersection
type combinator describes an intersection of multiple types. It takes zero or more type combinators as an input. The type that it describes is similar to a tuple instead of a TypeScript intersection. However, it behaves like an intersection instead of a tuple.
declare const pure: <A>(value: A) => Type<A>;
import { pure } from "typecraft";
The pure
type combinator describes a pure value. Typecasting pure
always succeeds with the value provided and it ignores its input.
declare const map: <A, B>(morphism: (a: A) => B, type: Type<A>) => Type<B>;
import { map } from "typecraft";
The map
type combinator transforms the result of another type combinator.
declare const fix: <A>(combinator: (type: Type<A>) => Type<A>) => Type<A>;
import type { Type } from "typecraft";
import { number, object, nullable, fix } from "typecraft";
type List<A> = Cons<A> | null;
interface Cons<A> {
head: A;
tail: List<A>;
}
const list = <A>(head: Type<A>) =>
fix<List<A>>((tail) => nullable(object({ head, tail })));
The fix
type combinator describes recursive types. It takes a single type endomorphism as an input and ties the knot to create a cyclic type.
declare type Cast<A> =
| { status: "success"; value: A; values: A[] }
| { status: "failure"; expected: "never"; actual: unknown }
| { status: "failure"; expected: "string"; actual: unknown }
| { status: "failure"; expected: "number"; actual: unknown }
| { status: "failure"; expected: "bigint"; actual: unknown }
| { status: "failure"; expected: "boolean"; actual: unknown }
| { status: "failure"; expected: "symbol"; actual: unknown }
| { status: "failure"; expected: "null"; actual: unknown }
| { status: "failure"; expected: "undefined"; actual: unknown }
| {
status: "failure";
expected: "array";
items?: Cast<unknown>[];
actual: unknown;
}
| {
status: "failure";
expected: "tuple";
length: number;
items?: Cast<unknown>[];
actual: unknown;
}
| {
status: "failure";
expected: "record";
properties?: Record<PropertyKey, Cast<unknown>>;
actual: unknown;
}
| {
status: "failure";
expected: "record";
properties?: Record<PropertyKey, Cast<unknown>>;
actual: unknown;
}
| {
status: "failure";
expected: "enumeration";
values: Set<Primitive>;
actual: unknown;
}
| { status: "failure"; expected: "union"; variants: Cast<never>[] }
| { status: "failure"; expected: "intersection"; results: Cast<unknown>[] };
declare const cast: <A>(type: Type<A>) => (input: unknown) => Cast<A>;
import { cast } from "typecraft";
The cast
function is used to create a typecasting function. It takes a Type<A>
as an input and returns a functions which typecasts values to A
.