https://github.com/peterboyer/pb.adt
Universal ADT utilities. (Formerly unenum).
https://github.com/peterboyer/pb.adt
adt typescript
Last synced: 7 months ago
JSON representation
Universal ADT utilities. (Formerly unenum).
- Host: GitHub
- URL: https://github.com/peterboyer/pb.adt
- Owner: peterboyer
- License: mit
- Created: 2023-03-25T01:11:27.000Z (almost 3 years ago)
- Default Branch: main
- Last Pushed: 2024-11-13T07:29:50.000Z (about 1 year ago)
- Last Synced: 2025-06-11T08:47:33.954Z (7 months ago)
- Topics: adt, typescript
- Language: TypeScript
- Homepage: https://npmjs.com/pb.adt
- Size: 779 KB
- Stars: 19
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Install
```shell
npm install pb.adt
```
## Requirements
- `typescript@>=5.0.0`
- `tsconfig.json > "compilerOptions" > { "strict": true }`
# Quickstart
`ADT` can create discriminated union types.
```ts
import { ADT } from "pb.adt";
type Post = ADT<{
Ping: true;
Text: { title?: string; body: string };
Photo: { url: string };
}>;
```
... which is identical to if you declared it manually.
```ts
type Post =
| { $type: "Ping" }
| { $type: "Text"; title?: string; body: string }
| { $type: "Photo"; url: string };
```
`ADT.define` can create discriminated union types and ease-of-use constructors.
```ts
const Post = ADT.define(
{} as {
Ping: true;
Text: { title?: string; body: string };
Photo: { url: string };
},
);
type Post = ADT.define;
```
Constructors can create ADT variant values:
- All constructed ADT variant values are plain objects.
- They match their variant types exactly.
- They do not have any methods or hidden properties.
```ts
const posts: Post[] = [
Post.Ping(),
Post.Text({ body: "Hello, World!" }),
Post.Photo({ url: "https://example.com/image.jpg" }),
];
```
The `ADT` provides ease-of-use utilities like `.switch` and `.match` for
working with discriminated unions.
```ts
(function (post: Post): string {
if (ADT.match(post, "Ping")) {
return "Ping!";
}
return ADT.switch(post, {
Text: ({ title }) => `Text("${title ?? "Untitled"}")`,
_: () => `Unhandled`,
});
});
```
`ADT` variant values are simple objects, you can narrow and access properties as
you would any other object.
```ts
function getTitleFromPost(post: Post): string | undefined {
return post.$type === "Text" ? post.title : undefined;
}
```
ADT supports creating discriminated unions with custom discriminants. (Click for details…)
```ts
type File = ADT<
{
"text/plain": { data: string };
"image/jpeg": { data: ImageBitmap };
"application/json": { data: unknown };
},
"mime"
>;
```
This creates a discriminated union identical to if you did so manually.
```ts
type File =
| { mime: "text/plain"; data: string }
| { mime: "image/jpeg"; data: ImageBitmap }
| { mime: "application/json"; data: unknown };
```
`ADT.*` methods for custom discriminants can be accessed via the `.on()` method.
```ts
const File = ADT.on("mime").define(
{} as {
"text/plain": { data: string };
"image/jpeg": { data: ImageBitmap };
"application/json": { data: unknown };
},
);
type File = ADT.define;
const files = [
File["text/plain"]({ data: "..." }),
File["image/jpeg"]({ data: new ImageBitmap() }),
File["application/json"]({ data: {} }),
];
(function (file: File): string {
if (ADT.on("mime").match(file, "text/plain")) {
return "Text!";
}
return ADT.on("mime").switch(file, {
"image/jpeg": ({ data }) => `Image(${data})`,
_: () => `Unhandled`,
});
});
```
---
# API
- [`ADT`](#adt)
- [`ADT.define`](#adtdefine)
- [`ADT.match`](#adtmatch)
- [`ADT.switch`](#adtswitch)
- [`ADT.value`](#adtvalue)
- [`ADT.unwrap`](#adtunwrap)
- [`ADT.on`](#adton)
- [`ADT.Root`](#adtroot)
- [`ADT.Keys`](#adtkeys)
- [`ADT.Pick`](#adtpick)
- [`ADT.Omit`](#adtomit)
- [`ADT.Extend`](#adtextend)
- [`ADT.Merge`](#adtmerge)
## `ADT`
```
(type) ADT
```
- Creates a discriminated union `type` from a key-value map of variants.
- Use `true` for unit variants that don't have any data properties ([not
`{}`](https://www.totaltypescript.com/the-empty-object-type-in-typescript)).
(Example) Using the default discriminant.
```ts
type Foo = ADT<{
Unit: true;
Data: { value: string };
}>;
```
(Example) Using a custom discriminant.
```ts
type Foo = ADT<
{
Unit: true;
Data: { value: string };
},
"custom"
>;
```
## `ADT.define`
```
(func) ADT.define(variants, options?: { [variant]: callback }) => builder
```
```ts
const Foo = ADT.define(
{} as {
Unit: true;
Data: { value: string };
},
);
type Foo = ADT.define;
```
## `ADT.match`
```
(func) ADT.match(value, variant | variants[]) => boolean
```
(Example) Match with one variant.
```ts
const foo = Foo.Unit() as Foo;
const value = ADT.match(foo, "Unit");
```
(Example) Match with many variants.
```ts
function getFileFormat(file: File): boolean {
const isText = ADT.on("mime").match(file, ["text/plain", "application/json"]);
return isText;
}
```
## `ADT.switch`
```
(func) ADT.switch(
value,
matcher = { [variant]: value | callback; _?: value | callback }
) => inferred
```
(Example) Handle all cases.
```ts
const foo: Foo = Foo.Unit() as Foo;
const value = ADT.switch(foo, {
Unit: "Unit()",
Data: ({ value }) => `Data(${value})`,
});
```
(Example) Unhandled cases with fallback.
```ts
const foo: Foo = Foo.Unit() as Foo;
const value = ADT.switch(foo, {
Unit: "Unit()",
_: "Unknown",
});
```
(Example) UI Framework (e.g. React) rendering all state cases.
```ts
const State = ADT.define(
{} as {
Pending: true;
Ok: { items: string[] };
Error: { cause: Error };
},
);
type State = ADT.define;
function Component(): Element {
const [state, setState] = useState(State.Pending());
// fetch data and exclusively handle success or error states
useEffect(() => {
(async () => {
const responseResult = await fetch("/items")
.then((response) => response.json() as Promise<{ items: string[] }>)
.catch((cause) =>
cause instanceof Error ? cause : new Error(undefined, { cause }),
);
setState(
responseResult instanceof Error
? State.Error({ cause: responseResult })
: State.Ok({ items: responseResult.items }),
);
})();
}, []);
// exhaustively handle all possible states
return ADT.switch(state, {
Loading: () => ``,
Ok: ({ items }) => `
- ${items.map(() => `
- `)}
Error: ({ cause }) => `Error: "${cause.message}"`,
});
}
```
## `ADT.value`
```
(func) ADT.value(variantName, variantProperties?) => inferred
```
- Useful if you add an additional ADT variant but don't have (or want to
define) a ADT builder for it.
(Example) Create an ADT value instance, (if possible) inferred from return type.
```ts
function getOutput(): ADT<{
None: true;
Some: { value: unknown };
All: true;
}> {
if (Math.random()) return ADT.value("All");
if (Math.random()) return ADT.value("Some", { value: "..." });
return ADT.value("None");
}
```
## `ADT.unwrap`
```
(func) ADT.unwrap(result, path) => inferred | undefined
```
- Extract a value's variant's property using a `"{VariantName}.{PropertyName}"`
path, otherwise returns `undefined`.
(Example) Safely wrap throwable function call, then unwrap the Ok variant's value or use a fallback.
```ts
const value = { $type: "A", foo: "..." } as ADT<{
A: { foo: string };
B: { bar: number };
}>;
const valueOrFallback = ADT.unwrap(value, "A.foo") ?? null;
```
## `ADT.on`
```
(func) ADT.on(discriminant) => { define, match, value, unwrap }
```
- Redefines and returns all `ADT.*` runtime methods with a custom discriminant.
(Example) Define and use an ADT with a custom discriminant.
```ts
const Foo = ADT.on("kind").define({} as { A: true; B: true });
type Foo = ADT.define;
const value = Foo.A() as Foo;
ADT.on("kind").match(value, "A");
ADT.on("kind").switch(value, { A: "A Variant", _: "Other Variant" });
```
## `ADT.Root`
```
(type) ADT.Root
```
(Example) Infer a key/value mapping of an ADT's variants.
```ts
export type Root = ADT.Root>;
// -> { Unit: true; Data: { value: string } }
```
## `ADT.Keys`
```
(type) ADT.Keys
```
(Example) Infers all keys of an ADT's variants.
```ts
export type Keys = ADT.Keys>;
// -> "Unit" | "Data"
```
## `ADT.Pick`
```
(type) ADT.Pick
```
(Example) Pick subset of an ADT's variants by key.
```ts
export type Pick = ADT.Pick<
ADT<{ Unit: true; Data: { value: string } }>,
"Unit"
>;
// -> { $type: "Unit" }
```
## `ADT.Omit`
```
(type) ADT.Omit
```
(Example) Omit subset of an ADT's variants by key.
```ts
export type Omit = ADT.Omit<
ADT<{ Unit: true; Data: { value: string } }>,
"Unit"
>;
// -> *Data
// -> *Green
```
## `ADT.Extend`
```
(type) ADT.Extend
```
(Example) Add new variants and merge new properties for existing variants for an ADT.
```ts
export type Extend = ADT.Extend<
ADT<{ Unit: true; Data: { value: string } }>,
{ Extra: true }
>;
// -> *Unit | *Data | *Extra
```
## `ADT.Merge`
```
(type) ADT.Merge
```
(Example) Merge all variants and properties of all given ADTs.
```ts
export type Merge = ADT.Merge | ADT<{ Right: true }>>;
// -> *Left | *Right
```