{"id":22619562,"url":"https://github.com/peterboyer/adt","last_synced_at":"2025-04-11T15:21:10.304Z","repository":{"id":151939695,"uuid":"618659017","full_name":"peterboyer/adt","owner":"peterboyer","description":"Universal ADT utilities. (Formerly unenum).","archived":false,"fork":false,"pushed_at":"2024-11-13T07:29:50.000Z","size":798,"stargazers_count":19,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-03T07:40:40.183Z","etag":null,"topics":["adt","typescript"],"latest_commit_sha":null,"homepage":"https://npmjs.com/pb.adt","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/peterboyer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-25T01:11:27.000Z","updated_at":"2025-03-26T15:30:51.000Z","dependencies_parsed_at":null,"dependency_job_id":"38aa1fb4-57ec-4586-bc8a-262cdf7d1966","html_url":"https://github.com/peterboyer/adt","commit_stats":null,"previous_names":["peterboyer/adt","peterboyer/unenum"],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterboyer%2Fadt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterboyer%2Fadt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterboyer%2Fadt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterboyer%2Fadt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/peterboyer","download_url":"https://codeload.github.com/peterboyer/adt/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248429129,"owners_count":21101786,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["adt","typescript"],"created_at":"2024-12-08T22:06:08.738Z","updated_at":"2025-04-11T15:21:10.281Z","avatar_url":"https://github.com/peterboyer.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# Install\n\n```shell\nnpm install pb.adt\n```\n\n## Requirements\n\n- `typescript@\u003e=5.0.0`\n- `tsconfig.json \u003e \"compilerOptions\" \u003e { \"strict\": true }`\n\n# Quickstart\n\n`ADT` can create discriminated union types.\n\n\n```ts\nimport { ADT } from \"pb.adt\";\n\ntype Post = ADT\u003c{\n  Ping: true;\n  Text: { title?: string; body: string };\n  Photo: { url: string };\n}\u003e;\n```\n\n\n... which is identical to if you declared it manually.\n\n\n```ts\ntype Post =\n  | { $type: \"Ping\" }\n  | { $type: \"Text\"; title?: string; body: string }\n  | { $type: \"Photo\"; url: string };\n```\n\n\n`ADT.define` can create discriminated union types and ease-of-use constructors.\n\n\n```ts\nconst Post = ADT.define(\n  {} as {\n    Ping: true;\n    Text: { title?: string; body: string };\n    Photo: { url: string };\n  },\n);\n\ntype Post = ADT.define\u003ctypeof Post\u003e;\n```\n\n\nConstructors can create ADT variant values:\n- All constructed ADT variant values are plain objects.\n- They match their variant types exactly.\n- They do not have any methods or hidden properties.\n\n\n```ts\nconst posts: Post[] = [\n  Post.Ping(),\n  Post.Text({ body: \"Hello, World!\" }),\n  Post.Photo({ url: \"https://example.com/image.jpg\" }),\n];\n```\n\n\nThe `ADT` provides ease-of-use utilities like `.switch` and `.match` for\nworking with discriminated unions.\n\n\n```ts\n(function (post: Post): string {\n  if (ADT.match(post, \"Ping\")) {\n    return \"Ping!\";\n  }\n\n  return ADT.switch(post, {\n    Text: ({ title }) =\u003e `Text(\"${title ?? \"Untitled\"}\")`,\n    _: () =\u003e `Unhandled`,\n  });\n});\n```\n\n\n`ADT` variant values are simple objects, you can narrow and access properties as\nyou would any other object.\n\n\n```ts\nfunction getTitleFromPost(post: Post): string | undefined {\n  return post.$type === \"Text\" ? post.title : undefined;\n}\n```\n\n\n\u003cdetails\u003e\u003csummary\u003e\u003ccode\u003eADT\u003c/code\u003e supports creating discriminated unions with custom discriminants. \u003csmall\u003e(Click for details…)\u003c/small\u003e\u003c/summary\u003e\n\u003cbr /\u003e\n\n\n```ts\ntype File = ADT\u003c\n  {\n    \"text/plain\": { data: string };\n    \"image/jpeg\": { data: ImageBitmap };\n    \"application/json\": { data: unknown };\n  },\n  \"mime\"\n\u003e;\n```\n\n\nThis creates a discriminated union identical to if you did so manually.\n\n\n```ts\ntype File =\n  | { mime: \"text/plain\"; data: string }\n  | { mime: \"image/jpeg\"; data: ImageBitmap }\n  | { mime: \"application/json\"; data: unknown };\n```\n\n\n`ADT.*` methods for custom discriminants can be accessed via the `.on()` method.\n\n\n```ts\nconst File = ADT.on(\"mime\").define(\n  {} as {\n    \"text/plain\": { data: string };\n    \"image/jpeg\": { data: ImageBitmap };\n    \"application/json\": { data: unknown };\n  },\n);\n\ntype File = ADT.define\u003ctypeof File\u003e;\n\nconst files = [\n  File[\"text/plain\"]({ data: \"...\" }),\n  File[\"image/jpeg\"]({ data: new ImageBitmap() }),\n  File[\"application/json\"]({ data: {} }),\n];\n\n(function (file: File): string {\n  if (ADT.on(\"mime\").match(file, \"text/plain\")) {\n    return \"Text!\";\n  }\n\n  return ADT.on(\"mime\").switch(file, {\n    \"image/jpeg\": ({ data }) =\u003e `Image(${data})`,\n    _: () =\u003e `Unhandled`,\n  });\n});\n```\n\n\n\u003c/details\u003e\n\n---\n\n# API\n\n- [`ADT`](#adt)\n  - [`ADT.define`](#adtdefine)\n  - [`ADT.match`](#adtmatch)\n  - [`ADT.switch`](#adtswitch)\n  - [`ADT.value`](#adtvalue)\n  - [`ADT.unwrap`](#adtunwrap)\n  - [`ADT.on`](#adton)\n  - [`ADT.Root`](#adtroot)\n  - [`ADT.Keys`](#adtkeys)\n  - [`ADT.Pick`](#adtpick)\n  - [`ADT.Omit`](#adtomit)\n  - [`ADT.Extend`](#adtextend)\n  - [`ADT.Merge`](#adtmerge)\n\n## `ADT`\n\n```\n(type) ADT\u003cTVariants, TDiscriminant?\u003e\n```\n\n- Creates a discriminated union `type` from a key-value map of variants.\n- Use `true` for unit variants that don't have any data properties ([not\n`{}`](https://www.totaltypescript.com/the-empty-object-type-in-typescript)).\n\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Using the default discriminant.\u003c/summary\u003e\n\n```ts\ntype Foo = ADT\u003c{\n  Unit: true;\n  Data: { value: string };\n}\u003e;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Using a custom discriminant.\u003c/summary\u003e\n\n```ts\ntype Foo = ADT\u003c\n  {\n    Unit: true;\n    Data: { value: string };\n  },\n  \"custom\"\n\u003e;\n```\n\n\u003c/details\u003e\n\n\u003cdiv align=right\u003e\u003ca href=#api\u003eBack to top ⤴\u003c/a\u003e\u003c/div\u003e\n\n## `ADT.define`\n\n```\n(func) ADT.define(variants, options?: { [variant]: callback }) =\u003e builder\n```\n\n\n```ts\nconst Foo = ADT.define(\n  {} as {\n    Unit: true;\n    Data: { value: string };\n  },\n);\n\ntype Foo = ADT.define\u003ctypeof Foo\u003e;\n```\n\n\n\u003cdiv align=right\u003e\u003ca href=#api\u003eBack to top ⤴\u003c/a\u003e\u003c/div\u003e\n\n## `ADT.match`\n\n```\n(func) ADT.match(value, variant | variants[]) =\u003e boolean\n```\n\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Match with one variant.\u003c/summary\u003e\n\n```ts\nconst foo = Foo.Unit() as Foo;\nconst value = ADT.match(foo, \"Unit\");\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Match with many variants.\u003c/summary\u003e\n\n```ts\nfunction getFileFormat(file: File): boolean {\n  const isText = ADT.on(\"mime\").match(file, [\"text/plain\", \"application/json\"]);\n  return isText;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdiv align=right\u003e\u003ca href=#api\u003eBack to top ⤴\u003c/a\u003e\u003c/div\u003e\n\n## `ADT.switch`\n\n```\n(func) ADT.switch(\n  value,\n  matcher = { [variant]: value | callback; _?: value | callback }\n) =\u003e inferred\n```\n\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Handle all cases.\u003c/summary\u003e\n\n```ts\nconst foo: Foo = Foo.Unit() as Foo;\nconst value = ADT.switch(foo, {\n  Unit: \"Unit()\",\n  Data: ({ value }) =\u003e `Data(${value})`,\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Unhandled cases with fallback.\u003c/summary\u003e\n\n```ts\nconst foo: Foo = Foo.Unit() as Foo;\nconst value = ADT.switch(foo, {\n  Unit: \"Unit()\",\n  _: \"Unknown\",\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) UI Framework (e.g. React) rendering all state cases.\u003c/summary\u003e\n\n```ts\nconst State = ADT.define(\n  {} as {\n    Pending: true;\n    Ok: { items: string[] };\n    Error: { cause: Error };\n  },\n);\n\ntype State = ADT.define\u003ctypeof State\u003e;\n\nfunction Component(): Element {\n  const [state, setState] = useState\u003cState\u003e(State.Pending());\n\n  // fetch data and exclusively handle success or error states\n  useEffect(() =\u003e {\n    (async () =\u003e {\n      const responseResult = await fetch(\"/items\")\n        .then((response) =\u003e response.json() as Promise\u003c{ items: string[] }\u003e)\n        .catch((cause) =\u003e\n          cause instanceof Error ? cause : new Error(undefined, { cause }),\n        );\n\n      setState(\n        responseResult instanceof Error\n          ? State.Error({ cause: responseResult })\n          : State.Ok({ items: responseResult.items }),\n      );\n    })();\n  }, []);\n\n  // exhaustively handle all possible states\n  return ADT.switch(state, {\n    Loading: () =\u003e `\u003cSpinner /\u003e`,\n    Ok: ({ items }) =\u003e `\u003cul\u003e${items.map(() =\u003e `\u003cli /\u003e`)}\u003c/ul\u003e`,\n    Error: ({ cause }) =\u003e `\u003cspan\u003eError: \"${cause.message}\"\u003c/span\u003e`,\n  });\n}\n```\n\n\u003c/details\u003e\n\n\u003cdiv align=right\u003e\u003ca href=#api\u003eBack to top ⤴\u003c/a\u003e\u003c/div\u003e\n\n## `ADT.value`\n\n```\n(func) ADT.value(variantName, variantProperties?) =\u003e inferred\n```\n\n- Useful if you add an additional ADT variant but don't have (or want to\ndefine) a ADT builder for it.\n\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Create an ADT value instance, (if possible) inferred from return type.\u003c/summary\u003e\n\n```ts\n\nfunction getOutput(): ADT\u003c{\n  None: true;\n  Some: { value: unknown };\n  All: true;\n}\u003e {\n  if (Math.random()) return ADT.value(\"All\");\n  if (Math.random()) return ADT.value(\"Some\", { value: \"...\" });\n  return ADT.value(\"None\");\n}\n```\n\n\u003c/details\u003e\n\n\u003cdiv align=right\u003e\u003ca href=#api\u003eBack to top ⤴\u003c/a\u003e\u003c/div\u003e\n\n## `ADT.unwrap`\n\n```\n(func) ADT.unwrap(result, path) =\u003e inferred | undefined\n```\n\n- Extract a value's variant's property using a `\"{VariantName}.{PropertyName}\"`\npath, otherwise returns `undefined`.\n\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Safely wrap throwable function call, then unwrap the Ok variant's value or use a fallback.\u003c/summary\u003e\n\n```ts\nconst value = { $type: \"A\", foo: \"...\" } as ADT\u003c{\n  A: { foo: string };\n  B: { bar: number };\n}\u003e;\nconst valueOrFallback = ADT.unwrap(value, \"A.foo\") ?? null;\n```\n\n\u003c/details\u003e\n\n\u003cdiv align=right\u003e\u003ca href=#api\u003eBack to top ⤴\u003c/a\u003e\u003c/div\u003e\n\n## `ADT.on`\n\n```\n(func) ADT.on(discriminant) =\u003e { define, match, value, unwrap }\n```\n\n- Redefines and returns all `ADT.*` runtime methods with a custom discriminant.\n\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Define and use an ADT with a custom discriminant.\u003c/summary\u003e\n\n```ts\nconst Foo = ADT.on(\"kind\").define({} as { A: true; B: true });\ntype Foo = ADT.define\u003ctypeof Foo\u003e;\n\nconst value = Foo.A() as Foo;\nADT.on(\"kind\").match(value, \"A\");\nADT.on(\"kind\").switch(value, { A: \"A Variant\", _: \"Other Variant\" });\n```\n\n\u003c/details\u003e\n\n\u003cdiv align=right\u003e\u003ca href=#api\u003eBack to top ⤴\u003c/a\u003e\u003c/div\u003e\n\n## `ADT.Root`\n\n```\n(type) ADT.Root\u003cTadt, TDiscriminant?\u003e\n```\n\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Infer a key/value mapping of an ADT's variants.\u003c/summary\u003e\n\n```ts\nexport type Root = ADT.Root\u003cADT\u003c{ Unit: true; Data: { value: string } }\u003e\u003e;\n// -\u003e { Unit: true; Data: { value: string } }\n```\n\n\u003c/details\u003e\n\n\u003cdiv align=right\u003e\u003ca href=#api\u003eBack to top ⤴\u003c/a\u003e\u003c/div\u003e\n\n## `ADT.Keys`\n\n```\n(type) ADT.Keys\u003cTadt, TDiscriminant?\u003e\n```\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Infers all keys of an ADT's variants.\u003c/summary\u003e\n\n```ts\nexport type Keys = ADT.Keys\u003cADT\u003c{ Unit: true; Data: { value: string } }\u003e\u003e;\n// -\u003e \"Unit\" | \"Data\"\n```\n\n\u003c/details\u003e\n\n\u003cdiv align=right\u003e\u003ca href=#api\u003eBack to top ⤴\u003c/a\u003e\u003c/div\u003e\n\n## `ADT.Pick`\n\n```\n(type) ADT.Pick\u003cTadt, TKeys, TDiscriminant?\u003e\n```\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Pick subset of an ADT's variants by key.\u003c/summary\u003e\n\n```ts\nexport type Pick = ADT.Pick\u003c\n  ADT\u003c{ Unit: true; Data: { value: string } }\u003e,\n  \"Unit\"\n\u003e;\n// -\u003e { $type: \"Unit\" }\n```\n\n\u003c/details\u003e\n\n\u003cdiv align=right\u003e\u003ca href=#api\u003eBack to top ⤴\u003c/a\u003e\u003c/div\u003e\n\n## `ADT.Omit`\n\n```\n(type) ADT.Omit\u003cTadt, TKeys, TDiscriminant?\u003e\n```\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Omit subset of an ADT's variants by key.\u003c/summary\u003e\n\n```ts\nexport type Omit = ADT.Omit\u003c\n  ADT\u003c{ Unit: true; Data: { value: string } }\u003e,\n  \"Unit\"\n\u003e;\n// -\u003e *Data\n\n// -\u003e *Green\n```\n\n\u003c/details\u003e\n\n\u003cdiv align=right\u003e\u003ca href=#api\u003eBack to top ⤴\u003c/a\u003e\u003c/div\u003e\n\n## `ADT.Extend`\n\n```\n(type) ADT.Extend\u003cTadt, TVariants, TDiscriminant?\u003e\n```\n\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Add new variants and merge new properties for existing variants for an ADT.\u003c/summary\u003e\n\n```ts\nexport type Extend = ADT.Extend\u003c\n  ADT\u003c{ Unit: true; Data: { value: string } }\u003e,\n  { Extra: true }\n\u003e;\n// -\u003e *Unit | *Data | *Extra\n```\n\n\u003c/details\u003e\n\n\u003cdiv align=right\u003e\u003ca href=#api\u003eBack to top ⤴\u003c/a\u003e\u003c/div\u003e\n\n## `ADT.Merge`\n\n```\n(type) ADT.Merge\u003cTadts, TDiscriminant?\u003e\n```\n\n\u003cdetails\u003e\u003csummary\u003e(\u003cstrong\u003eExample\u003c/strong\u003e) Merge all variants and properties of all given ADTs.\u003c/summary\u003e\n\n```ts\nexport type Merge = ADT.Merge\u003cADT\u003c{ Left: true }\u003e | ADT\u003c{ Right: true }\u003e\u003e;\n// -\u003e *Left | *Right\n```\n\n\u003c/details\u003e\n\n\u003cdiv align=right\u003e\u003ca href=#api\u003eBack to top ⤴\u003c/a\u003e\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterboyer%2Fadt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpeterboyer%2Fadt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterboyer%2Fadt/lists"}