typecraft
Version:
library for crafting types and casting type inhabitants
493 lines (372 loc) • 12.6 kB
Markdown
# Typecraft

[](https://github.com/aaditmshah/typecraft/blob/main/LICENSE)

[](https://github.com/semantic-release/semantic-release)
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Faaditmshah%2Ftypecraft)
Typecraft is a library for performing type-level magic. For example, you can use it to safely typecast values.
```typescript
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"](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/). Hence, typecasting is more powerful than simple data validation. For example, because `Type` is functorial you can [transfigure](https://harrypotter.fandom.com/wiki/Transfiguration) 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.
```typescript
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)
})
);
```
## API
### `Type`
```typescript
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`.
### `Typedef`
```typescript
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.
### `unknown`
```typescript
declare const unknown: Type<unknown>;
```
```typescript
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.
### `never`
```typescript
declare const never: Type<never>;
```
```typescript
import { never } from "typecraft";
```
The `never` type combinator describes values which don't exist. Typecasting `never` returns a functions which always fails.
### `string`
```typescript
declare const string: Type<string>;
```
```typescript
import { string } from "typecraft";
```
The `string` type combinator describes strings. Typecasting `string` returns a function which only succeeds if the input is a string.
### `number`
```typescript
declare const number: Type<number>;
```
```typescript
import { number } from "typecraft";
```
The `number` type combinator describes numbers. Typecasting `number` returns a function which only succeeds if the input is a number.
### `bigint`
```typescript
declare const bigint: Type<bigint>;
```
```typescript
import { bigint } from "typecraft";
```
The `bigint` type combinator describes bigints. Typecasting `bigint` returns a function which only succeeds if the input is a bigint.
### `boolean`
```typescript
declare const boolean: Type<boolean>;
```
```typescript
import { boolean } from "typecraft";
```
The `boolean` type combinator describes booleans. Typecasting `boolean` returns a function which only succeeds if the input is a boolean.
### `symbol`
```typescript
declare const symbol: Type<symbol>;
```
```typescript
import { symbol } from "typecraft";
```
The `symbol` type combinator describes symbols. Typecasting `symbol` returns a function which only succeeds if the input is a symbol.
### `primitive`
```typescript
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]>;
```
```typescript
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`.
### `array`
```typescript
declare const array: <A>(type: Type<A>) => Type<A[]>;
```
```typescript
import { array } from "typecraft";
```
The `array` type combinator describes arrays. It takes a single type combinator, describing items of the array, as an input.
### `tuple`
```typescript
declare type Types<A> = {
[T in keyof A]: Type<A[T]>;
};
declare const tuple: <A extends unknown[]>(...types: Types<A>) => Type<A>;
```
```typescript
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.
### `record`
```typescript
declare const record: <A>(type: Type<A>) => Type<Record<PropertyKey, A>>;
```
```typescript
import { record } from "typecraft";
```
The `record` type combinator describes records. It takes a single type combinator, describing values of the record, as an input.
### `object`
```typescript
declare type Types<A> = {
[T in keyof A]: Type<A[T]>;
};
declare const object: <A extends {}>(propTypes: Types<A>) => Type<A>;
```
```typescript
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.
### `nullable`
```typescript
declare const nullable: <A>(type: Type<A>) => Type<A | null>;
```
```typescript
import { nullable } from "typecraft";
```
The `nullable` type combinator describes nullable types. It takes a single type combinator as an input.
### `optional`
```typescript
declare const optional: <A>(type: Type<A>) => Type<A | undefined>;
```
```typescript
import { optional } from "typecraft";
```
The `optional` type combinator describes optional types. It takes a single type combinator as an input.
### `enumeration`
```typescript
declare type Primitive =
| string
| number
| bigint
| boolean
| symbol
| null
| undefined;
declare const enumeration: <A extends Primitive[]>(
...values: A
) => Type<A[number]>;
```
```typescript
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.
### `union`
```typescript
declare type Types<A> = {
[T in keyof A]: Type<A[T]>;
};
declare const union: <A extends unknown[]>(
...types: Types<A>
) => Type<A[number]>;
```
```typescript
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.
### `intersection`
```typescript
declare type Types<A> = {
[T in keyof A]: Type<A[T]>;
};
declare const intersection: <A extends unknown[]>(
...types: Types<A>
) => Type<A>;
```
```typescript
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.
### `pure`
```typescript
declare const pure: <A>(value: A) => Type<A>;
```
```typescript
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.
### `map`
```typescript
declare const map: <A, B>(morphism: (a: A) => B, type: Type<A>) => Type<B>;
```
```typescript
import { map } from "typecraft";
```
The `map` type combinator transforms the result of another type combinator.
### `fix`
```typescript
declare const fix: <A>(combinator: (type: Type<A>) => Type<A>) => Type<A>;
```
```typescript
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.
### `cast`
```typescript
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>;
```
```typescript
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`.