1.0.1 • Published 8 months ago

pb.adt v1.0.1

Weekly downloads
-
License
MIT
Repository
github
Last release
8 months ago

Install

npm install pb.adt

Requirements

  • typescript@>=5.0.0
  • tsconfig.json > "compilerOptions" > { "strict": true }

Quickstart

ADT can create discriminated union types.

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.

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.

const Post = ADT.define(
  {} as {
    Ping: true;
    Text: { title?: string; body: string };
    Photo: { url: string };
  },
);

type Post = ADT.define<typeof Post>;

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

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

function getTitleFromPost(post: Post): string | undefined {
  return post.$type === "Text" ? post.title : undefined;
}
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.

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.

const File = ADT.on("mime").define(
  {} as {
    "text/plain": { data: string };
    "image/jpeg": { data: ImageBitmap };
    "application/json": { data: unknown };
  },
);

type File = ADT.define<typeof File>;

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

(type) ADT<TVariants, TDiscriminant?>
  • 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 {}).
type Foo = ADT<{
  Unit: true;
  Data: { value: string };
}>;
type Foo = ADT<
  {
    Unit: true;
    Data: { value: string };
  },
  "custom"
>;

ADT.define

(func) ADT.define(variants, options?: { [variant]: callback }) => builder
const Foo = ADT.define(
  {} as {
    Unit: true;
    Data: { value: string };
  },
);

type Foo = ADT.define<typeof Foo>;

ADT.match

(func) ADT.match(value, variant | variants[]) => boolean
const foo = Foo.Unit() as Foo;
const value = ADT.match(foo, "Unit");
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
const foo: Foo = Foo.Unit() as Foo;
const value = ADT.switch(foo, {
  Unit: "Unit()",
  Data: ({ value }) => `Data(${value})`,
});
const foo: Foo = Foo.Unit() as Foo;
const value = ADT.switch(foo, {
  Unit: "Unit()",
  _: "Unknown",
});
const State = ADT.define(
  {} as {
    Pending: true;
    Ok: { items: string[] };
    Error: { cause: Error };
  },
);

type State = ADT.define<typeof State>;

function Component(): Element {
  const [state, setState] = useState<State>(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: () => `<Spinner />`,
    Ok: ({ items }) => `<ul>${items.map(() => `<li />`)}</ul>`,
    Error: ({ cause }) => `<span>Error: "${cause.message}"</span>`,
  });
}

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.
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.
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.
const Foo = ADT.on("kind").define({} as { A: true; B: true });
type Foo = ADT.define<typeof Foo>;

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<Tadt, TDiscriminant?>
export type Root = ADT.Root<ADT<{ Unit: true; Data: { value: string } }>>;
// -> { Unit: true; Data: { value: string } }

ADT.Keys

(type) ADT.Keys<Tadt, TDiscriminant?>
export type Keys = ADT.Keys<ADT<{ Unit: true; Data: { value: string } }>>;
// -> "Unit" | "Data"

ADT.Pick

(type) ADT.Pick<Tadt, TKeys, TDiscriminant?>
export type Pick = ADT.Pick<
  ADT<{ Unit: true; Data: { value: string } }>,
  "Unit"
>;
// -> { $type: "Unit" }

ADT.Omit

(type) ADT.Omit<Tadt, TKeys, TDiscriminant?>
export type Omit = ADT.Omit<
  ADT<{ Unit: true; Data: { value: string } }>,
  "Unit"
>;
// -> *Data

// -> *Green

ADT.Extend

(type) ADT.Extend<Tadt, TVariants, TDiscriminant?>
export type Extend = ADT.Extend<
  ADT<{ Unit: true; Data: { value: string } }>,
  { Extra: true }
>;
// -> *Unit | *Data | *Extra

ADT.Merge

(type) ADT.Merge<Tadts, TDiscriminant?>
export type Merge = ADT.Merge<ADT<{ Left: true }> | ADT<{ Right: true }>>;
// -> *Left | *Right
1.0.1

8 months ago

1.0.0

8 months ago

1.0.0-sha.d801c5c

8 months ago

1.0.0-sha.0183a4e

9 months ago

3.0.0-sha.ec69ddd

9 months ago

0.0.0

9 months ago