Package Exports
- unenum
Readme
Install
npm install unenumRequirements
typescript@>=5.0.0tsconfig.json > "compilerOptions" > { "strict": true }
Quickstart
Enum can create discriminated union types.
import { Enum } from "unenum";
type Post = Enum<{
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 };Enum.define can create discriminated union types and ease-of-use constructors.
const Post = Enum.define(
{} as {
Ping: true;
Text: { title?: string; body: string };
Photo: { url: string };
},
);
type Post = Enum.define<typeof Post>;Constructors can create Enum variant values:
- All constructed Enum 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 Enum provides ease-of-use utilities like .match for working with
discriminated unions.
(function (post: Post): string {
if (Enum.match(post, "Ping")) {
return "Ping!";
}
return Enum.match(post, {
Text: ({ title }) => `Text("${title ?? "Untitled"}")`,
_: () => `Unhandled`,
});
});Enum 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;
}Enum supports creating discriminated unions with custom discriminants. (Click for details…)
type File = Enum<
{
"text/plain": { data: string };
"image/jpeg": { data: Buffer };
"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: Buffer }
| { mime: "application/json"; data: unknown };Enum.* methods for custom discriminants can be accessed via the .on() method.
const File = Enum.on("mime").define(
{} as {
"text/plain": { data: string };
"image/jpeg": { data: Buffer };
"application/json": { data: unknown };
},
);
type File = Enum.define<typeof File>;
const files = [
File["text/plain"]({ data: "..." }),
File["image/jpeg"]({ data: Buffer.from("...") }),
File["application/json"]({ data: {} }),
];
(function (file: File): string {
if (Enum.on("mime").match(file, "text/plain")) {
return "Text!";
}
return Enum.on("mime").match(file, {
"image/jpeg": ({ data }) => `Image(${data.length})`,
_: () => `Unhandled`,
});
});Result creates a discriminated union with an Ok and Error variant.
import { Result } from "unenum";
export async function getUserCountFromDatabase(): Promise<
Result<number, "DatabaseError">
> {
const queriedCount = await Promise.resolve(1);
return Result.Ok(queriedCount);
}... which is identical to if you declared it manually.
export async function getUserCountFromDatabase(): Promise<
{ _type: "Ok"; value: number } | { _type: "Error"; error: "DatabaseError" }
> {
const queriedCount = await Promise.resolve(1);
return { _type: "Ok", value: queriedCount };
}Result.from calls a given callback that could throw and returns a Result
variant value:
Result.Okwith the callback's return value,Result.Errorwith the callback's thrown error as a value.
export async function getUserCountFromDatabase(): Promise<
Result<number, "DatabaseError">
> {
// Database query "throws" if database is unreachable or query fails.
const $queriedCount = await Result.from(() => queryDatabaseUserCount());
// Handle error, forward cause.
if (Enum.match($queriedCount, "Error")) {
return Result.Error("DatabaseError", $queriedCount);
}
return Result.Ok($queriedCount.value);
}Real-world example. (Click for details…)
export function getTokens(): Tokens | undefined {
// Retrieve a JSON string to be parsed.
const tokensSerialised = window.localStorage.getItem("tokens") ?? undefined;
if (!tokensSerialised) {
return undefined;
}
// JSON.parse "throws" if given an invalid JSON string.
const $tokensUnknown = Result.from(() => JSON.parse(tokensSerialised));
if (Enum.match($tokensUnknown, "Error")) {
return undefined;
}
// Tokens.parse "throws" if given a value that doesn't match the schema.
const $tokens = Result.from(() => Tokens.parse($tokensUnknown.value));
if (Enum.match($tokens, "Error")) {
return undefined;
}
return $tokens.value;
}
import { z } from "zod";
const Tokens = z.object({ accessToken: z.string(), refreshToken: z.string() });
type Tokens = z.infer<typeof Tokens>;API
Enum
(type) Enum<TVariants, TDiscriminant?>- Creates a discriminated union
typefrom a key-value map of variants. - Use
truefor unit variants that don't have any data properties (not{}).
(Example) Using the default discriminant.
type Foo = Enum<{
Unit: true;
Data: { value: string };
}>;(Example) Using a custom discriminant.
type Foo = Enum<
{
Unit: true;
Data: { value: string };
},
"custom"
>;Enum.define
(func) Enum.define(variants, options?: { [variant]: callback }) => builderconst Foo = Enum.define(
{} as {
Unit: true;
Data: { value: string };
},
);
type Foo = Enum.define<typeof Foo>;Enum.match
(func) Enum.match(value, variant | variants[]) => boolean
(func) Enum.match(value, matcher = { [variant]: value | callback; _?: value | callback }) => inferred(Example) Narrow with one variant.
const foo = Foo.Unit() as Foo;
const value = Enum.match(foo, "Unit");(Example) Narrow with many variants.
function getFileFormat(file: File): boolean {
const isText = Enum.on("mime").match(file, [
"text/plain",
"application/json",
]);
return isText;
}(Example) Handle all cases.
const foo: Foo = Foo.Unit() as Foo;
const value = Enum.match(foo, {
Unit: "Unit()",
Data: ({ value }) => `Data(${value})`,
});(Example) Unhandled cases with fallback.
const foo: Foo = Foo.Unit() as Foo;
const value = Enum.match(foo, {
Unit: "Unit()",
_: "Unknown",
});Enum.value
(func) Enum.value(variantName, variantProperties?) => inferred- Useful if you add an additional Enum variant but don't have (or want to define) a Enum builder for it.
(Example) Create an Enum value instance, (if possible) inferred from return type.
function getOutput(): Enum<{
None: true;
Some: { value: unknown };
All: true;
}> {
if (Math.random()) return Enum.value("All");
if (Math.random()) return Enum.value("Some", { value: "..." });
return Enum.value("None");
}Enum.unwrap
(func) Enum.unwrap(result, path) => inferred | undefined- Extract a value's variant's property using a
"{VariantName}.{PropertyName}"path, otherwise returnsundefined.
(Example) Safely wrap throwable function call, then unwrap the Ok variant's value or use a fallback.
const result = Result.from(() => JSON.stringify("..."));
const valueOrFallback = Enum.unwrap(result, "Ok.value") ?? null;Enum.on
(func) Enum.on(discriminant) => { define, match, value, unwrap }- Redefines and returns all
Enum.*runtime methods with a custom discriminant.
(Example) Define and use an Enum with a custom discriminant.
const Foo = Enum.on("kind").define({} as { A: true; B: true });
type Foo = Enum.define<typeof Foo>;
const value = Foo.A() as Foo;
Enum.on("kind").match(value, "A");
Enum.on("kind").match(value, { A: "A Variant", _: "Other Variant" });Enum.Root
(type) Enum.Root<TEnum, TDiscriminant?>(Example) Infer a key/value mapping of an Enum's variants.
export type Root = Enum.Root<Enum<{ Unit: true; Data: { value: string } }>>;
// -> { Unit: true; Data: { value: string } }Enum.Keys
(type) Enum.Keys<TEnum, TDiscriminant?>(Example) Infers all keys of an Enum's variants.
export type Keys = Enum.Keys<Enum<{ Unit: true; Data: { value: string } }>>;
// -> "Unit" | "Data"Enum.Pick
(type) Enum.Pick<TEnum, TKeys, TDiscriminant?>(Example) Pick subset of an Enum's variants by key.
export type Pick = Enum.Pick<
Enum<{ Unit: true; Data: { value: string } }>,
"Unit"
>;
// -> { _type: "Unit" }Enum.Omit
(type) Enum.Omit<TEnum, TKeys, TDiscriminant?>(Example) Omit subset of an Enum's variants by key.
export type Omit = Enum.Omit<
Enum<{ Unit: true; Data: { value: string } }>,
"Unit"
>;
// -> *Data
// -> *GreenEnum.Extend
(type) Enum.Extend<TEnum, TVariants, TDiscriminant?>(Example) Add new variants and merge new properties for existing variants for an Enum.
export type Extend = Enum.Extend<
Enum<{ Unit: true; Data: { value: string } }>,
{ Extra: true }
>;
// -> *Unit | *Data | *ExtraEnum.Merge
(type) Enum.Merge<TEnums, TDiscriminant?>(Example) Merge all variants and properties of all given Enums.
export type Merge = Enum.Merge<Enum<{ Left: true }> | Enum<{ Right: true }>>;
// -> *Left | *RightResult
(type) Result<TOk?, TError?>- A helper alias for
Result.Ok | Result.Error.
[!NOTE] This "Errors As Values" pattern allows known error cases to handled in a type-safe way, as opposed to
throwing errors and relying on the caller to remember to wrap it intry/catch.
(Example) Result without any values.
export function getResult(): Result {
const isValid = Math.random();
if (!isValid) {
return Result.Error();
}
return Result.Ok();
}(Example) Result with Ok and Error values.
export function queryFile(): Result<File, "NotFound"> {
const fileOrUndefined = getFile();
if (fileOrUndefined) {
return Result.Error("NotFound");
}
return Result.Ok(file);
}Result.Ok
(type) Enum.Ok<TOk?>
(func) Enum.Ok(inferred) => Enum.Ok<inferred>- Represents a normal/success value,
{ _type: "Ok"; value: "..." }.
Result.Error
(type) Enum.Error<TError?>
(func) Enum.Error(inferred, cause?) => Enum.Error<inferred>- Represents an error/failure value,
{ _type: "Error"; error: "..."; cause?: ... }.
(func) Result.from(callback)- Executes the callback within a
try/catch:- returns a
Enum.Okwith the callback's result, - otherwise a
Enum.Errorwith the thrown error (if any).
- returns a
(Example) Wrap a function that may throw.
const fetchResult = await Result.from(() => fetch("/api/whoami"));
Enum.match(fetchResult, {
Ok: async ({ value: response }) => {
const body = (await response.json()) as unknown;
console.log(body);
},
Error: ({ error }) => {
console.error(error);
},
});Pending
(type) Pending
(func) Pending() => Pending- Represents an pending state.
- Ideal for states' values or stateful functions (like React hooks).
(Example) React hook that returns a value, error, or pending state.
import { Pending } from "unenum";
function useFetchedListItems(): Result<string[], "NetworkError"> | Pending {
const { data, error, loading } = useQuery(gqlListItems);
if (loading) {
return Pending();
}
if (error || !data) {
return Result.Error("NetworkError");
}
return Result.Ok(data.gqlListItems.items);
}(Example) React state that could be a loaded value, error, or loading state.
function Component(): Element {
const [state, setState] = useState<
Result<{ items: string[] }, "NetworkError"> | Pending
>(Pending());
// fetch data and exclusively handle success or error states
useEffect(() => {
(async () => {
const responseResult = await Result.from(() =>
fetch("/items").then(
(response) => response.json() as Promise<{ items: string[] }>,
),
);
if (Enum.match(responseResult, "Error")) {
setState(Result.Error("NetworkError"));
return;
}
setState(Result.Ok({ items: responseResult.value.items }));
return;
})();
}, []);
// exhaustively handle all possible states
return Enum.match(state, {
Loading: () => `<Spinner />`,
Ok: ({ value: { items } }) => `<ul>${items.map(() => `<li />`)}</ul>`,
Error: ({ error }) => `<span>A ${error} error has occurred.</span>`,
});
}