matchable
Version:
A utility to define and match on tagged unions (like enums with payloads) — safely.
189 lines (182 loc) • 5.47 kB
TypeScript
/**
* A mapping of variant names to their payload factory functions.
*
* Each key defines a variant constructor that returns an object payload.
* Used as the input to `matchable()`.
*/
type VariantMap = Record<string, (...args: any[]) => Record<string, any>>;
/**
* Infers the union type from a `VariantMap`.
*
* Wraps each payload in a `{ tag: ... }` object for pattern matching.
*/
type InferUnion<M extends VariantMap> = {
[K in keyof M]: M[K] extends (...args: any[]) => infer R ? R & {
tag: K;
} : never;
}[keyof M & string];
/**
* A handler map for `match()`, where each tag maps to a function.
*
* Can optionally include a `default` fallback.
*/
type MatchHandlers<U extends {
tag: string;
}, R> = {
[K in Exclude<U["tag"], "default">]?: (value: Extract<U, {
tag: K;
}>) => R;
} & {
default?: (value: U) => R;
};
/**
* A more flexible match handler shape that allows partial matching
* as long as a `default` handler is provided.
*/
type MatchWithDefault<U extends {
tag: string;
}, R> = MatchHandlers<U, R> | (Partial<Omit<MatchHandlers<U, R>, "default">> & {
default: (value: U) => R;
});
/**
* Extracts the union type from a matchable instance.
*
* @example
* const Result = matchable({
* Ok: (value: number) => ({ value }),
* Err: (message: string) => ({ message }),
* });
*
* type ResultType = MatchableOf<typeof Result>;
* // => { tag: "Ok"; value: number } | { tag: "Err"; message: string }
*/
type MatchableOf<T> = T extends {
match: (value: infer U, ...args: any[]) => any;
} ? U : never;
/**
* Extracts a specific variant from a matchable union.
*
* @example
* type OkVariant = VariantOf<typeof Result, "Ok">;
* // => { tag: "Ok"; value: number }
*/
type VariantOf<T, Tag extends string> = Extract<MatchableOf<T>, {
tag: Tag;
}>;
/**
* Extracts the union of all possible `tag` values from a matchable instance.
*
* @example
* const Result = matchable({
* Ok: (value: number) => ({ value }),
* Err: (error: string) => ({ error }),
* });
*
* type ResultTags = TagsOf<typeof Result>;
* // => "Ok" | "Err"
*/
type TagsOf<T> = MatchableOf<T> extends {
tag: infer Tag;
} ? Tag : never;
/**
* Merges multiple match handlers into a single handler map,
* preserving the shape expected by `.match()`.
*
* Useful when multiple tags share logic and should map
* to the same function. You can still provide a `default`
* fallback for any unhandled tags.
*
* @param matchable - A matchable instance with `_tags` and `match()`
*
* @param handlers - A partial set of handlers for specific tags, plus optional `default`
*
* @returns A `MatchWithDefault` handler map compatible with `match()`
*
* @example
* const handlers = group(Result, {
* Ok: handleSuccess,
* Cached: handleSuccess,
* Err: handleError,
* });
*
* const message = Result.match(value, handlers);
*/
declare function group<T extends {
match: (value: any, handlers: any) => any;
}, U extends MatchableOf<T>, R>(matchable: T & {
_tags: readonly string[];
}, handlers: {
[K in Exclude<U["tag"], "default">]?: (value: Extract<U, {
tag: K;
}>) => R;
} & {
default?: (value: U) => R;
}): MatchWithDefault<U, R>;
/**
* Validates if a value matches one of the known tags.
*
* Useful for safely checking data loaded from storage, APIs, etc.
*
* @param matchable - The matchable instance with `_tags`
*
* @param value - The value to check
*
* @returns `true` if the value has a valid tag
*/
declare function isValid<T extends {
_tags: readonly string[];
}>(matchable: T, value: unknown): value is {
tag: T["_tags"][number];
};
declare const MATCHABLE_INTERNAL_ID: unique symbol;
declare function serialize<M extends VariantMap>(value: InferUnion<M>): string;
/**
* Creates a tagged union and a type-safe matcher.
*
* Each key in `variants` becomes a constructor function that returns
* an object with a `tag` and a payload. Use `match()` to handle variants
* exhaustively, or include a `default` handler for partial matching.
*
* @param variants - A map of variant names to factory functions.
*
* @returns An object with:
* - constructors for each variant
* - a `match()` function for safe pattern matching
* - a `is` object with type guards
* - `_tags` listing supported tags
*
* @example
* const Result = matchable({
* Ok: (value: number) => ({ value }),
* Err: (message: string) => ({ message }),
* });
*
* const result = Result.Ok(42);
* const msg = Result.match(result, {
* Ok: ({ value }) => `✅ ${value}`,
* Err: ({ message }) => `❌ ${message}`,
* });
*/
declare function matchable<const M extends VariantMap>(variants: M): { [K in keyof M & string]: (...args: Parameters<M[K]>) => ReturnType<M[K]> & {
__matchable_id__: typeof MATCHABLE_INTERNAL_ID;
tag: K;
}; } & {
_tags: (keyof M & string)[];
deserialize: (json: string) => {
tag: keyof M & string;
};
is: { [K_1 in keyof M & string]: (value: unknown) => value is {
tag: K_1;
}; } & {
Valid: (value: unknown) => value is {
tag: keyof M & string;
};
};
match: <R>(value: InferUnion<M> & {
__matchable_id__?: symbol;
}, handlers: MatchWithDefault<InferUnion<M> & {
__matchable_id__?: symbol;
}, R>) => R;
serialize: typeof serialize;
};
export { type MatchableOf, type TagsOf, type VariantOf, group, isValid, matchable };