An open API service indexing awesome lists of open source software.

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).

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"
>;
```

Back to top ⤴

## `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;
```

Back to top ⤴

## `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;
}
```

Back to top ⤴

## `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}"`,
});
}
```

Back to top ⤴

## `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");
}
```

Back to top ⤴

## `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;
```

Back to top ⤴

## `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" });
```

Back to top ⤴

## `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 } }
```

Back to top ⤴

## `ADT.Keys`

```
(type) ADT.Keys
```
(Example) Infers all keys of an ADT's variants.

```ts
export type Keys = ADT.Keys>;
// -> "Unit" | "Data"
```

Back to top ⤴

## `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" }
```

Back to top ⤴

## `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
```

Back to top ⤴

## `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
```

Back to top ⤴

## `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
```

Back to top ⤴