ts-pattern

repository·main·Indexed 12 days ago

https://github.com/gvergnaud/ts-pattern

An exhaustive pattern matching library for TypeScript (v5.9.0) providing smart type inference and compile-time safety. It features data structure matching for objects, arrays, and primitives, exhaustiveness checking via .exhaustive(), and advanced logic using wildcards (P._), predicates (P.when), and property selection (P.select()).

Tokens
19K
Snippets
90
Records
94
Agent score
95%

What's inside ts-pattern

  1. Core features of ts-pattern

    main

    ts-pattern provides a typesafe way to express complex branching logic with the following capabilities:

    • Data Structure Matching: Match on Objects, Arrays, Tuples, Sets, Maps, and primitives.
    • Exhaustiveness Checking: Use .exhaustive() to enforce that all possible cases are covered.
    • Validation: Use isMatching to validate if data matches a specific pattern.
    • Expressive Wildcards: Use P._ for catch-all or type-specific wildcards like P.string and P.number.
    • Advanced Logic: Supports predicates (P.when), unions, intersections, and exclusions (P.not).
    • Property Selection: Use P.select() to extract specific values from a matched pattern into the callback argument.
    • Small Footprint: The library is approximately 2kB.
  2. Match Objects with sub-patterns

    main

    An object pattern matches if and only if the input value is an object, contains all properties defined in the pattern, and each property matches its corresponding sub-pattern.

    import { match } from 'ts-pattern';
    
    type Input =
      | { type: 'user'; name: string }
      | { type: 'image'; src: string }
      | { type: 'video'; seconds: number };
    
    let input: Input = { type: 'user', name: 'Gabriel' };
    
    const output = match(input)
      .with({ type: 'image' }, () => 'image')
      .with({ type: 'video', seconds: 10 }, () => 'video of 10 seconds.')
      .with({ type: 'user' }, ({ name }) => `user of name: ${name}`)
      .otherwise(() => 'something else');
    
    console.log(output);
    // => 'user of name: Gabriel'
  3. Match Tuples (arrays) with fixed length

    main

    A tuple pattern matches if the input value is an array of the same length and each item matches the corresponding sub-pattern.

    import { match, P } from 'ts-pattern';
    
    type Input =
      | [number, '+', number]
      | [number, '-', number]
      | [number, '*', number]
      | ['-', number];
    
    const input = [3, '*', 4] as Input;
    
    const output = match(input)
      .with([P._, '+', P._], ([x, , y]) => x + y)
      .with([P._, '-', P._], ([x, , y]) => x - y)
      .with([P._, '*', P._], ([x, , y]) => x * y)
      .with(['-', P._], ([, x]) => -x)
      .exhaustive();
    
    console.log(output);
    // => 12
  4. Advanced Type Inference: `P.select` and Type Guards

    main

    TS-Pattern provides advanced type inference capabilities:

    P.select()

    When using P.select() within a pattern, TS-Pattern extracts and injects the selected value into your handler function with the correct type.

    Type Guard Functions

    If you pass a TypeScript type guard function to P.when(), TS-Pattern uses the return type of that function to narrow the input type within the handler.

    Exhaustiveness Checking

    By calling .exhaustive(), TS-Pattern ensures that all possible cases of a union type are handled. If a case is missing, it will throw a NonExhaustiveError at compile time.

    const isString = (x: unknown): x is string => typeof x === 'string';
    
    const fn = (input: { id: number | string }) =>
      match(input)
        .with({ id: P.when(isString) }, (narrowed /* : { id: string } */) => 'yes')
        .with({ id: P.when(isNumber) }, (narrowed /* : { id: number } */) => 'yes')
        .exhaustive();
  5. Migrate from TS-Pattern v4 to v5

    main

    When upgrading to v5, be aware of the following breaking changes:

    1. TypeScript Requirement: TS-Pattern v5 requires TypeScript v5+ due to its use of const type parameters.
    2. Eager Evaluation: In v4, handlers were lazy and only executed when .exhaustive() or .otherwise() was called. In v5, .with() handlers are evaluated eagerly as soon as a match is found. To ensure predictable behavior, always terminate your pattern matching expressions with .exhaustive() or .otherwise().
    3. Map and Set Matching: You can no longer match Set or Map instances directly using .with(new Set(...)) or .with(new Map(...)). Instead, use the P.set() and P.map() patterns.
    import { match, P } from 'ts-pattern';
    
    const someFunction = (value: Set<number> | Map<string, number>) =>
      match(value)
        .with(P.set(P.number), (set) => `a set of numbers`)
        .with(P.map('key', P.number), (map) => `map.get('key') is a number`)
        .otherwise(() => null);
  6. Install ts-pattern

    main

    You can install ts-pattern using npm or any of your preferred package managers.

    npm install ts-pattern

    Or using other managers:

    pnpm add ts-pattern
    # OR
    yarn add ts-pattern
    # OR
    bun add ts-pattern
    # OR
    npx jsr add @gabriel/ts-pattern
  7. Migrate from TS-Pattern v3 to v4

    main

    If you are upgrading from version 3 to version 4, several breaking changes require updates to your imports and pattern syntax:

    1. Updated Imports

    Type-specific wildcards and pattern creation functions have moved from top-level exports to the Pattern (or P) module.

    • Type-specific wildcards: Use Pattern.string or P.string instead of __.string.
    • Top-level wildcards: Use P._ or P.any instead of __.
    • Pattern functions: Use P.select(), P.not(), and P.when() instead of the standalone select(), not(), and when().
    • Pattern Type: The Pattern type is now accessed via P.Pattern.

    2. List Patterns

    Matching arrays of unknown length has changed. The syntax [subpattern] now matches arrays with exactly one element (consistent with native destructuring). To match arrays of any length, use P.array(subpattern).

    3. NaN Matching

    Instead of using __.NaN, simply use the native NaN value in your .with() clause.

    - import { match, __ } from 'ts-pattern';
    + import { match, P } from 'ts-pattern';
    
     const toString = (value: string | number) =>
       match(value)
    -   .with(__.string, (v) => v)
    -   .with(__.number, (v) => `${v}`)
    +   .with(P.string, (v) => v)
    +   .with(P.number, (v) => `${v}`)
         .exhaustive();
  8. Match using Literals

    main

    You can match against primitive JavaScript values like numbers, strings, booleans, bigints, symbols, null, undefined, or NaN. When using match(input), the .with() method accepts these literals to trigger specific handler functions.

    import { match } from 'ts-pattern';
    
    const input: unknown = 2;
    
    const output = match(input)
      .with(2, () => 'number: two')
      .with(true, () => 'boolean: true')
      .with('hello', () => 'string: hello')
      .with(undefined, () => 'undefined')
      .with(null, () => 'null')
      .with(NaN, () => 'number: NaN')
      .with(20n, () => 'bigint: 20n')
      .otherwise(() => 'something else');
    
    console.log(output);
    // => 'number: two'
  9. Basic usage of match() and exhaustive()

    main

    Use match(value) to start a pattern matching expression. You can chain .with(pattern, callback) to handle specific cases. To ensure you have handled every possible case in a union type, terminate the chain with .exhaustive(). This provides compile-time safety by alerting you if a case is missing.

    import { match, P } from 'ts-pattern';
    
    type Data = 
      | { type: 'text'; content: string } 
      | { type: 'img'; src: string };
    
    type Result = 
      | { type: 'ok'; data: Data } 
      | { type: 'error'; error: Error };
    
    const result: Result = ...;
    
    const html = match(result)
      .with({ type: 'error' }, () => <p>Oups! An error occured</p>)
      .with({ type: 'ok', data: { type: 'text' } }, (res) => <p>{res.data.content}</p>)
      .with({ type: 'ok', data: { type: 'img', src: P.select() } }, (src) => <img src={src} />)
      .exhaustive();
  10. Example: Implementing a state reducer with ts-pattern

    main

    A common use case for ts-pattern is implementing a state reducer that branches on both the current State and an incoming Event. This allows you to define valid state transitions (e.g., only allowing a cancel event when the state is loading) without nested switch statements.

    import { match, P } from 'ts-pattern';
    
    type State =
      | { status: 'idle' }
      | { status: 'loading'; startTime: number }
      | { status: 'success'; data: string }
      | { status: 'error'; error: Error };
    
    type Event =
      | { type: 'fetch' }
      | { type: 'success'; data: string }
      | { type: 'error'; error: Error }
      | { type: 'cancel' };
    
    const reducer = (state: State, event: Event): State =>
      match([state, event])
        .returnType<State>()
        .with(
          [{ status: 'loading' }, { type: 'success' }],
          ([_, event]) => ({ status: 'success', data: event.data })
        )
        .with(
          [{ status: 'loading' }, { type: 'error', error: P.select() }],
          (error) => ({ status: 'error', error })
        )
        .with(
          [{ status: P.not('loading') }, { type: 'fetch' }],
          () => ({ status: 'loading', startTime: Date.now() })
        )
        .with(
          [
            {
              status: 'loading',
              startTime: P.when((t) => t + 2000 < Date.now()),
            },
            { type: 'cancel' },
          ],
          () => ({ status: 'idle' })
        )
        .with(P._, () => state)
        .exhaustive();
  11. Annotate optional keys with `P.optional`

    main

    P.optional(subpattern) allows you to match an object key that might be undefined, but if it is present, it must match the provided subpattern.

    import { match, P } from 'ts-pattern';
    
    type Input = { key?: string | number };
    const input: Input = { key: 'hello' };
    
    const output = match(input)
      .with({ key: P.optional(P.string) }, (a) => a.key) // string | undefined
      .with({ key: P.optional(P.number) }, (a) => a.key) // number | undefined
      .exhaustive();
  12. Match numbers and bigints with `P.number` and `P.bigint` predicates

    main

    The P.number and P.bigint predicates allow for precise numeric matching:

    • P.number.between(min, max): Matches numbers between min and max (inclusive).
    • P.number.lt(max): Matches numbers strictly less than max.
    • P.number.gt(min): Matches numbers strictly greater than min.
    • P.number.lte(max): Matches numbers less than or equal to max.
    • P.number.gte(min): Matches numbers greater than or equal to min.
    • P.number.int(): Matches integers.
    • P.number.finite(): Matches all numbers except Infinity and -Infinity.
    • P.number.positive(): Matches positive numbers.
    • P.number.negative(): Matches negative numbers.
    const fn = (input: number) =>
      match(input)
        .with(P.number.between(1, 5), () => '✅')
        .otherwise(() => '❌');
    
    console.log(fn(3), fn(1), fn(5), fn(7)); // logs '✅ ✅ ✅ ❌'