@monstermann/match
Version:
Zero-runtime exhaustive pattern matching.
364 lines (357 loc) • 15.2 kB
TypeScript
import { ConditionalPick, IsLiteral, OptionalKeysOf, Primitive, RequireAtLeastOne, RequiredKeysOf, Simplify } from "type-fest";
//#region src/internals/helpers.d.ts
type IsTrue<T> = true extends T ? true : false;
type IfTrue<T, OnTrue, OnFalse> = true extends T ? OnTrue : OnFalse;
type Union<A, B> = [B] extends [A] ? A : [A] extends [B] ? B : A | B;
type IsLiteral$1<T> = IsLiteral<T> extends true ? true : T extends null | undefined ? true : false;
interface MatchError<i> {
__: never;
}
//#endregion
//#region src/internals/case.d.ts
type HasCases<T> = IsTrue<T extends unknown ? T extends Primitive | null | undefined ? true : false : false>;
type PickCase<T, U> = IsLiteral$1<U> extends true ? U : Extract<T, U>;
type DropCase<T, U> = IsLiteral$1<U> extends true ? Exclude<T, U> : T;
type CasePatterns<T> = T extends unknown ? T extends Primitive ? T : never : never;
type JoinMatchedCases<T, U> = IsLiteral$1<U> extends true ? T | U : T;
type ExcludeMatchedCases<T, U> = Exclude<T, U>;
//#endregion
//#region src/internals/shape.d.ts
type PlainObject = { [Key in PropertyKey]: unknown };
type HasShapes<T> = IsTrue<T extends unknown ? T extends PlainObject ? true : false : false>;
type PickShape<T, U> = T extends unknown ? T extends PlainObject ? Readonly<Simplify<MatchShape<T, U> & U>> : never : never;
type DropShape<T, U> = T extends unknown ? T extends PlainObject ? [MatchShape<T, U>] extends [never] ? T : Simplify<CompleteShape<T & { [K in keyof U]: K extends keyof T ? U[K] extends T[K] ? IsLiteral$1<U[K]> extends true ? Exclude<T[K], U[K]> : T[K] : never : never }>> : T : never;
type ShapePatterns<T> = T extends unknown ? T extends PlainObject ? AtLeastOneField<CleanOptionals<ConditionalPick<T, Primitive>>> : never : never;
type JoinMatchedShapes<T, U> = HasLiteralFields<U> extends true ? T | U : T;
type ExcludeMatchedShapes<T, U> = [U] extends [never] ? T : U extends unknown ? [MatchShape<U, T>] extends [never] ? T : never : never;
type MatchShape<T, U> = IfTrue<{ [K in keyof U]: K extends keyof T ? U[K] extends T[K] ? true : false : false }[keyof U], T, never>;
type CompleteShape<T> = IfTrue<{ [K in keyof T]: [T[K]] extends [never] ? true : false }[keyof T], never, T>;
type CleanOptionals<T extends object> = Simplify<{ [K in RequiredKeysOf<T>]: T[K] } & { [K in OptionalKeysOf<T>]: T[K] | undefined }>;
type AtLeastOneField<T> = RequireAtLeastOne<T, keyof T>;
type HasLiteralFields<T> = IsTrue<{ [K in keyof T]: IsLiteral$1<T[K]> }[keyof T]>;
//#endregion
//#region src/internals/MatchStrict.d.ts
interface MatchStrict<Input, Output = never, MatchedLiterals = never, MatchedShapes = never> {
returnType: MatchError<"Calling `.returnType<T>()` is only allowed directly after `match(...)`">;
/**
* Matches a primitive value (`===`). Returns result if matched.
*
* ```ts
* const value = 2 as number
*
* match(value)
* .case(1, "one")
* .case(2, "two")
* .case(3, "three")
* .orThrow() //=> "two"
* ```
*/
case: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasCases<Input> extends true ? <const I extends CasePatterns<Input>>(value: ExcludeMatchedCases<I, MatchedLiterals>, result: Output) => MatchStrict<DropCase<Input, I>, Output, JoinMatchedCases<MatchedLiterals, I>, MatchedShapes> : MatchError<"No primitives left to match against">;
/**
* Like `.case`, but calls `fn(value)` if matched. Useful for expensive computations.
*
* ```ts
* const value = 2 as number;
*
* match(value)
* .onCase(1, (num) => num * -1)
* .onCase(2, (num) => num * -2)
* .onCase(3, (num) => num * -3)
* .orThrow(); //=> -4
* ```
*/
onCase: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasCases<Input> extends true ? <const I extends CasePatterns<Input>>(value: ExcludeMatchedCases<I, MatchedLiterals>, fn: (value: PickCase<Input, I>) => Output) => MatchStrict<DropCase<Input, I>, Output, JoinMatchedCases<MatchedLiterals, I>, MatchedShapes> : MatchError<"No primitives left to match against">;
/**
* Matches a shallow object shape. All fields must match (`===`), only supports matching primitives.
*
* ```ts
* type Rectangle = {
* x: number
* y: number
* width: number
* height: number
* }
*
* type Circle = {
* x: number
* y: number
* radius: number
* }
*
* const value: Rectangle = {
* x: 0,
* y: 0,
* width: 0,
* height: 0,
* }
*
* const isEmpty = match(value as Rectangle | Circle)
* .shape({ width: 0, height: 0 }, true)
* .shape({ radius: 0 }, true)
* .or(false) //=> true
* ```
*/
shape: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasShapes<Input> extends true ? <const I extends ShapePatterns<Input>>(value: ExcludeMatchedShapes<I, MatchedShapes>, result: Output) => MatchStrict<DropShape<Input, I>, Output, MatchedLiterals, JoinMatchedShapes<MatchedShapes, I>> : MatchError<"No shapes left to match against">;
/**
* Like `.shape`, but calls `fn(value)` if matched. Useful for expensive computations.
*
* ```ts
* type Rectangle = {
* kind: "rectangle"
* width: number
* height: number
* }
*
* type Circle = {
* kind: "circle"
* radius: number
* }
*
* const value: Rectangle = {
* kind: "rectangle",
* width: 10,
* height: 10,
* }
*
* const area = match(value as Rectangle | Circle)
* .shape({ kind: "rectangle" }, rect => rect.width * rect.height)
* .shape({ kind: "circle" }, circ => Math.PI * circ.radius ** 2)
* .orThrow() //=> 100
* ```
*/
onShape: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasShapes<Input> extends true ? <const I extends ShapePatterns<Input>>(value: ExcludeMatchedShapes<I, MatchedShapes>, fn: (value: PickShape<Input, I>) => Output) => MatchStrict<DropShape<Input, I>, Output, MatchedLiterals, JoinMatchedShapes<MatchedShapes, I>> : MatchError<"No shapes left to match against">;
/**
* Matches if `predicate(value)` is truthy.
*
* ```ts
* match(10 as number)
* .cond((num) => num > 0, "positive")
* .cond((num) => num < 0, "negative")
* .or("zero"); //=> "positive"
* ```
*/
cond: [Input] extends [never] ? MatchError<"Match is exhaustive"> : (<const I extends Input>(predicate: (value: Input) => value is I, result: Output) => MatchStrict<Exclude<Input, I>, Output, MatchedLiterals, MatchedShapes>) & ((predicate: (value: Input) => boolean, result: Output) => MatchStrict<Input, Output, MatchedLiterals, MatchedShapes>);
/**
* Like `.cond`, but calls `fn(value)` if matched.
*
* <!-- prettier-ignore -->
* ```ts
* match("Hello world!" as string)
* .onCond(msg => msg.length > 250, msg => `Message "${msg}" is too long`)
* .onCond(msg => msg.length < 100, msg => `Message "${msg}" is too short`)
* .or(false); //=> `Message "Hello world!" is too short`
* ```
*/
onCond: [Input] extends [never] ? MatchError<"Match is exhaustive"> : (<const I extends Input>(predicate: (value: Input) => value is I, fn: (value: I) => Output) => MatchStrict<Exclude<Input, I>, Output, MatchedLiterals, MatchedShapes>) & ((predicate: (value: Input) => boolean, fn: (value: Input) => Output) => MatchStrict<Input, Output, MatchedLiterals, MatchedShapes>);
/**
* Returns the result, otherwise the given fallback.
*
* ```ts
* import { match } from "/match";
*
* match(3 as number)
* .case(1, "one")
* .case(2, "two")
* .or("other"); //=> "other"
* ```
*/
or: [Input] extends [never] ? MatchError<"Match is exhaustive"> : (fallback: Output) => Output;
/**
* Returns the result, otherwise calls `fn(value)`.
*
* ```ts
* import { match } from "/match";
*
* match(3 as number)
* .case(1, "one")
* .case(2, "two")
* .orElse((num) => String(num)); //=> "3"
* ```
*/
orElse: [Input] extends [never] ? MatchError<"Match is exhaustive"> : (fallback: (value: Readonly<Input>) => Output) => Output;
/**
* Returns the result, or throws an exception at runtime. Enforces exhaustiveness at compile time.
*
* ```ts
* import { match } from "/match";
*
* match(3 as number)
* .case(1, "one")
* .case(2, "two")
* .orThrow(); //=> Error
* // ~~~~~~~ ❌ Type 'MatchError<3>' has no call signatures.
* ```
*/
orThrow: [Input] extends [never] ? () => Output : MatchError<Input>;
}
//#endregion
//#region src/internals/Match.d.ts
interface Match<Input, Output = never, MatchedLiterals = never, MatchedShapes = never> {
/**
* By default, the return type is inferred from your cases. You can enforce a specific type:
*
* ```ts
* match(foo)
* .returnType<"a" | "b" | "c">()
* .case(1, "a")
* .case(2, "b")
* .or("c")
* ```
*/
returnType: [Output] extends [never] ? <O>() => MatchStrict<Input, O> : MatchError<"Calling `.returnType<T>()` is only allowed directly after `match(...)`">;
/**
* Matches a primitive value (`===`). Returns result if matched.
*
* ```ts
* const value = 2 as number
*
* match(value)
* .case(1, "one")
* .case(2, "two")
* .case(3, "three")
* .orThrow() //=> "two"
* ```
*/
case: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasCases<Input> extends true ? <const I extends CasePatterns<Input>, O>(value: ExcludeMatchedCases<I, MatchedLiterals>, result: O) => Match<DropCase<Input, I>, Union<Output, O>, JoinMatchedCases<MatchedLiterals, I>, MatchedShapes> : MatchError<"No primitives left to match against">;
/**
* Like `.case`, but calls `fn(value)` if matched. Useful for expensive computations.
*
* ```ts
* const value = 2 as number;
*
* match(value)
* .onCase(1, (num) => num * -1)
* .onCase(2, (num) => num * -2)
* .onCase(3, (num) => num * -3)
* .orThrow(); //=> -4
* ```
*/
onCase: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasCases<Input> extends true ? <const I extends CasePatterns<Input>, O>(value: ExcludeMatchedCases<I, MatchedLiterals>, fn: (value: PickCase<Input, I>) => O) => Match<DropCase<Input, I>, Union<Output, O>, JoinMatchedCases<MatchedLiterals, I>, MatchedShapes> : MatchError<"No primitives left to match against">;
/**
* Matches a shallow object shape. All fields must match (`===`), only supports matching primitives.
*
* ```ts
* type Rectangle = {
* x: number
* y: number
* width: number
* height: number
* }
*
* type Circle = {
* x: number
* y: number
* radius: number
* }
*
* const value: Rectangle = {
* x: 0,
* y: 0,
* width: 0,
* height: 0,
* }
*
* const isEmpty = match(value as Rectangle | Circle)
* .shape({ width: 0, height: 0 }, true)
* .shape({ radius: 0 }, true)
* .or(false) //=> true
* ```
*/
shape: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasShapes<Input> extends true ? <const I extends ShapePatterns<Input>, O>(value: ExcludeMatchedShapes<I, MatchedShapes>, result: O) => Match<DropShape<Input, I>, Union<Output, O>, MatchedLiterals, JoinMatchedShapes<MatchedShapes, I>> : MatchError<"No shapes left to match against">;
/**
* Like `.shape`, but calls `fn(value)` if matched. Useful for expensive computations.
*
* ```ts
* type Rectangle = {
* kind: "rectangle"
* width: number
* height: number
* }
*
* type Circle = {
* kind: "circle"
* radius: number
* }
*
* const value: Rectangle = {
* kind: "rectangle",
* width: 10,
* height: 10,
* }
*
* const area = match(value as Rectangle | Circle)
* .shape({ kind: "rectangle" }, rect => rect.width * rect.height)
* .shape({ kind: "circle" }, circ => Math.PI * circ.radius ** 2)
* .orThrow() //=> 100
* ```
*/
onShape: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasShapes<Input> extends true ? <const I extends ShapePatterns<Input>, O>(value: ExcludeMatchedShapes<I, MatchedShapes>, fn: (value: PickShape<Input, I>) => O) => Match<DropShape<Input, I>, Union<Output, O>, MatchedLiterals, JoinMatchedShapes<MatchedShapes, I>> : MatchError<"No shapes left to match against">;
/**
* Matches if `predicate(value)` is truthy.
*
* ```ts
* match(10 as number)
* .cond((num) => num > 0, "positive")
* .cond((num) => num < 0, "negative")
* .or("zero"); //=> "positive"
* ```
*/
cond: [Input] extends [never] ? MatchError<"Match is exhaustive"> : (<const I extends Input, O>(predicate: (value: Input) => value is I, result: O) => Match<Exclude<Input, I>, Union<Output, O>, MatchedLiterals, MatchedShapes>) & (<O>(predicate: (value: Input) => boolean, result: O) => Match<Input, Union<Output, O>, MatchedLiterals, MatchedShapes>);
/**
* Like `.cond`, but calls `fn(value)` if matched.
*
* <!-- prettier-ignore -->
* ```ts
* match("Hello world!" as string)
* .onCond(msg => msg.length > 250, msg => `Message "${msg}" is too long`)
* .onCond(msg => msg.length < 100, msg => `Message "${msg}" is too short`)
* .or(false); //=> `Message "Hello world!" is too short`
* ```
*/
onCond: [Input] extends [never] ? MatchError<"Match is exhaustive"> : (<const I extends Input, O>(predicate: (value: Input) => value is I, fn: (value: I) => O) => Match<Exclude<Input, I>, Union<Output, O>, MatchedLiterals, MatchedShapes>) & (<O>(predicate: (value: Input) => boolean, fn: (value: Input) => O) => Match<Input, Union<Output, O>, MatchedLiterals, MatchedShapes>);
/**
* Returns the result, otherwise the given fallback.
*
* ```ts
* import { match } from "/match";
*
* match(3 as number)
* .case(1, "one")
* .case(2, "two")
* .or("other"); //=> "other"
* ```
*/
or: [Input] extends [never] ? MatchError<"Match is exhaustive"> : <O>(fallback: O) => Union<Output, O>;
/**
* Returns the result, otherwise calls `fn(value)`.
*
* ```ts
* import { match } from "/match";
*
* match(3 as number)
* .case(1, "one")
* .case(2, "two")
* .orElse((num) => String(num)); //=> "3"
* ```
*/
orElse: [Input] extends [never] ? MatchError<"Match is exhaustive"> : <O>(fallback: (value: Readonly<Input>) => O) => Union<Output, O>;
/**
* Returns the result, or throws an exception at runtime. Enforces exhaustiveness at compile time.
*
* ```ts
* import { match } from "/match";
*
* match(3 as number)
* .case(1, "one")
* .case(2, "two")
* .orThrow(); //=> Error
* // ~~~~~~~ ❌ Type 'MatchError<3>' has no call signatures.
* ```
*/
orThrow: [Input] extends [never] ? () => Output : MatchError<Input>;
}
//#endregion
//#region src/match.d.ts
declare function match<const T>(value: T): Match<T>;
//#endregion
export { match };