UNPKG

@monstermann/match

Version:

Zero-runtime exhaustive pattern matching.

364 lines (357 loc) 15.2 kB
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 "@monstermann/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 "@monstermann/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 "@monstermann/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 "@monstermann/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 "@monstermann/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 "@monstermann/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 };