GithubHelp home page GithubHelp logo

gvergnaud / ts-pattern Goto Github PK

View Code? Open in Web Editor NEW
10.9K 31.0 113.0 2.18 MB

🎨 The exhaustive Pattern Matching library for TypeScript, with smart type inference.

License: MIT License

JavaScript 0.01% TypeScript 99.83% Shell 0.15%
pattern-matching typescript ts pattern matching inference type-inference exhaustive conditions branching

ts-pattern's Introduction

TS-Pattern

The exhaustive Pattern Matching library for TypeScript with smart type inference.

downloads npm version MIT license

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();

About

Write better and safer conditions. Pattern matching lets you express complex conditions in a single, compact expression. Your code becomes shorter and more readable. Exhaustiveness checking ensures you haven’t forgotten any possible case.

ts-pattern

Animation by @nicoespeon

Features

What is Pattern Matching?

Pattern Matching is a code-branching technique coming from functional programming languages that's more powerful and often less verbose than imperative alternatives (if/else/switch statements), especially for complex conditions.

Pattern Matching is implemented in Haskell, Rust, Swift, Elixir, and many other languages. There is a tc39 proposal to add Pattern Matching to EcmaScript, but it is still in stage 1 and isn't likely to land before several years. Luckily, pattern matching can be implemented in userland. ts-pattern Provides a typesafe pattern matching implementation that you can start using today.

Read the introduction blog post: Bringing Pattern Matching to TypeScript 🎨 Introducing TS-Pattern

Installation

Via npm

npm install ts-pattern

Via yarn

yarn add ts-pattern

Via pnpm

pnpm add ts-pattern

Via Bun

bun add ts-pattern

Compatibility with different TypeScript versions

TS-Pattern assumes that Strict Mode is enabled in your tsconfig.json file.

ts-pattern TypeScript v5+ TypeScript v4.5+ TypeScript v4.2+
v5.x (Docs) (Migration Guide) βœ… ❌ ❌
v4.x (Docs) (Migration Guide) βœ… βœ… ❌
v3.x (Docs) βœ… βœ… βœ…
  • βœ… Full support
  • ❌ Not supported

Want to learn how to build awesome, strongly-typed libraries?

Check out πŸ‘‰ Type-Level TypeScript, my online course teaching how to take full advantage of the most advanced features of TypeScript. You will learn everything there is to know to build awesome libraries with great developer experiences and become a real TypeScript expert in the process!

Documentation

Code Sandbox Examples

Getting Started

As an example, let's create a state reducer for a frontend application that fetches some data.

Example: a state reducer with ts-pattern

Our application can be in four different states: idle, loading, success and error. Depending on which state we are in, some events can occur. Here are all the possible types of event our application can respond to: fetch, success, error and cancel.

I use the word event but you can replace it with action if you are used to Redux's terminology.

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' };

Even though our application can handle 4 events, only a subset of these events make sense for each given state. For instance we can only cancel a request if we are currently in the loading state. To avoid unwanted state changes that could lead to bugs, we want our state reducer function to branch on both the state and the event, and return a new state.

This is a case where match really shines. Instead of writing nested switch statements, we can use pattern matching to simultaneously check the state and the event object:

import { match, P } from 'ts-pattern';

const reducer = (state: State, event: Event) =>
  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();

There's a lot going on, so let's go through this code bit by bit:

match(value)

match takes a value and returns a builder on which you can add your pattern matching cases.

match([state, event])

It's also possible to specify the input and output type explicitly with match<Input, Output>(...), but this is usually unnecessary, as TS-Pattern is able to infer them.

.returnType<OutputType>()

.returnType is an optional method that you can call if you want to force all following code-branches to return a value of a specific type. It takes a single type parameter, provided between <AngleBrackets>.

  .returnType<State>()

Here, we use this method to make sure all branches return a valid State object.

.with(pattern, handler)

Then we add a first with clause:

  .with(
    [{ status: 'loading' }, { type: 'success' }],
    ([state, event]) => ({
      // `state` is inferred as { status: 'loading' }
      // `event` is inferred as { type: 'success', data: string }
      status: 'success',
      data: event.data,
    })
  )

The first argument is the pattern: the shape of value you expect for this branch.

The second argument is the handler function: the code branch that will be called if the input value matches the pattern.

The handler function takes the input value as first parameter with its type narrowed down to what the pattern matches.

P.select(name?)

In the second with clause, we use the P.select function:

  .with(
    [
      { status: 'loading' },
      { type: 'error', error: P.select() }
    ],
    (error) => ({ status: 'error', error })
  )

P.select() lets you extract a piece of your input value and inject it into your handler. It is pretty useful when pattern matching on deep data structures because it avoids the hassle of destructuring your input in your handler.

Since we didn't pass any name to P.select(), It will inject the event.error property as first argument to the handler function. Note that you can still access the full input value with its type narrowed by your pattern as second argument of the handler function:

  .with(
    [
      { status: 'loading' },
      { type: 'error', error: P.select() }
    ],
    (error, stateAndEvent) => {
      // error: Error
      // stateAndEvent: [{ status: 'loading' }, { type: 'error', error: Error }]
    }
  )

In a pattern, we can only have a single anonymous selection. If you need to select more properties on your input data structure, you will need to give them names:

.with(
    [
      { status: 'success', data: P.select('prevData') },
      { type: 'error', error: P.select('err') }
    ],
    ({ prevData, err }) => {
      // Do something with (prevData: string) and (err: Error).
    }
  )

Each named selection will be injected inside a selections object, passed as first argument to the handler function. Names can be any strings.

P.not(pattern)

If you need to match on everything but a specific value, you can use a P.not(<pattern>) pattern. it's a function taking a pattern and returning its opposite:

  .with(
    [{ status: P.not('loading') }, { type: 'fetch' }],
    () => ({ status: 'loading' })
  )

P.when() and guard functions

Sometimes, we need to make sure our input value respects a condition that can't be expressed by a pattern. For example, imagine you need to check that a number is positive. In these cases, we can use guard functions: functions taking a value and returning a boolean.

With TS-Pattern, there are two ways to use a guard function:

  • use P.when(<guard function>) inside one of your patterns
  • pass it as second parameter to .with(...)

using P.when(predicate)

  .with(
    [
      {
        status: 'loading',
        startTime: P.when((t) => t + 2000 < Date.now()),
      },
      { type: 'cancel' },
    ],
    () => ({ status: 'idle' })
  )

Passing a guard function to .with(...)

.with optionally accepts a guard function as second parameter, between the pattern and the handler callback:

  .with(
    [{ status: 'loading' }, { type: 'cancel' }],
    ([state, event]) => state.startTime + 2000 < Date.now(),
    () => ({ status: 'idle' })
  )

This pattern will only match if the guard function returns true.

the P._ wildcard

P._ will match any value. You can use it either at the top level, or within another pattern.

  .with(P._, () => state)

  // You could also use it inside another pattern:
  .with([P._, P._], () => state)

  // at any level:
  .with([P._, { type: P._ }], () => state)

.exhaustive(), .otherwise() and .run()

  .exhaustive();

.exhaustive() executes the pattern matching expression, and returns the result. It also enables exhaustiveness checking, making sure we don't forget any possible case in our input value. This extra type safety is very nice because forgetting a case is an easy mistake to make, especially in an evolving code-base.

Note that exhaustive pattern matching is optional. It comes with the trade-off of having slightly longer compilation times because the type checker has more work to do.

Alternatively, you can use .otherwise(), which takes an handler function returning a default value. .otherwise(handler) is equivalent to .with(P._, handler).exhaustive().

  .otherwise(() => state);

Matching several patterns

As you may know, switch statements allow handling several cases with the same code block:

switch (type) {
  case 'text':
  case 'span':
  case 'p':
    return 'text';

  case 'btn':
  case 'button':
    return 'button';
}

Similarly, ts-pattern lets you pass several patterns to .with() and if one of these patterns matches your input, the handler function will be called:

const sanitize = (name: string) =>
  match(name)
    .with('text', 'span', 'p', () => 'text')
    .with('btn', 'button', () => 'button')
    .otherwise(() => name);

sanitize('span'); // 'text'
sanitize('p'); // 'text'
sanitize('button'); // 'button'

As you might expect, this also works with more complex patterns than strings and exhaustiveness checking works as well.

API Reference

match

match(value);

Create a Match object on which you can later call .with, .when, .otherwise and .run.

Signature

function match<TInput, TOutput>(input: TInput): Match<TInput, TOutput>;

Arguments

  • input
    • Required
    • the input value your patterns will be tested against.

.with

match(...)
  .with(pattern, [...patterns], handler)

Signature

function with(
  pattern: Pattern<TInput>,
  handler: (selections: Selections<TInput>, value: TInput) => TOutput
): Match<TInput, TOutput>;

// Overload for multiple patterns
function with(
  pattern1: Pattern<TInput>,
  ...patterns: Pattern<TInput>[],
  // no selection object is provided when using multiple patterns
  handler: (value: TInput) => TOutput
): Match<TInput, TOutput>;

// Overload for guard functions
function with(
  pattern: Pattern<TInput>,
  when: (value: TInput) => unknown,
  handler: (
    selection: Selection<TInput>,
    value: TInput
  ) => TOutput
): Match<TInput, TOutput>;

Arguments

  • pattern: Pattern<TInput>
    • Required
    • The pattern your input must match for the handler to be called.
    • See all valid patterns below
    • If you provide several patterns before providing the handler, the with clause will match if one of the patterns matches.
  • when: (value: TInput) => unknown
    • Optional
    • Additional condition the input must satisfy for the handler to be called.
    • The input will match if your guard function returns a truthy value.
    • TInput might be narrowed to a more precise type using the pattern.
  • handler: (selections: Selections<TInput>, value: TInput) => TOutput
    • Required
    • Function called when the match conditions are satisfied.
    • All handlers on a single match case must return values of the same type, TOutput.
    • selections is an object of properties selected from the input with the select function.
    • TInput might be narrowed to a more precise type using the pattern.

.when

match(...)
  .when(predicate, handler)

Signature

function when(
  predicate: (value: TInput) => unknown,
  handler: (value: TInput) => TOutput
): Match<TInput, TOutput>;

Arguments

  • predicate: (value: TInput) => unknown
    • Required
    • Condition the input must satisfy for the handler to be called.
  • handler: (value: TInput) => TOutput
    • Required
    • Function called when the predicate condition is satisfied.
    • All handlers on a single match case must return values of the same type, TOutput.

.returnType

match(...)
  .returnType<string>()
  .with(..., () => "has to be a string")
  .with(..., () => "Oops".length)
  //               ~~~~~~~~~~~~~ ❌ `number` isn't a string!

The .returnType<SomeType>() method allows you to control the return type of all of your branches of code. It accepts a single type parameter that will be used as the return type of your match expression. All code branches must return values assignable to this type.

Signature

function returnType<TOutputOverride>(): Match<TInput, TOutputOverride>;

Type arguments

  • TOutputOverride
    • The type that your match expression will return. All branches must return values assignable to it.

.exhaustive

match(...)
  .with(...)
  .exhaustive()

Runs the pattern-matching expression and returns its result. It also enables exhaustiveness checking, making sure at compile time that we have handled all possible cases.

Signature

function exhaustive(): TOutput;

Example

type Permission = 'editor' | 'viewer';
type Plan = 'basic' | 'pro';

const fn = (org: Plan, user: Permission) =>
  match([org, user])
    .with(['basic', 'viewer'], () => {})
    .with(['basic', 'editor'], () => {})
    .with(['pro', 'viewer'], () => {})
    // Fails with `NonExhaustiveError<['pro', 'editor']>`
    // because the `['pro', 'editor']` case isn't handled.
    .exhaustive();

const fn2 = (org: Plan, user: Permission) =>
  match([org, user])
    .with(['basic', 'viewer'], () => {})
    .with(['basic', 'editor'], () => {})
    .with(['pro', 'viewer'], () => {})
    .with(['pro', 'editor'], () => {})
    .exhaustive(); // Works!

.otherwise

match(...)
  .with(...)
  .otherwise(defaultHandler)

Runs the pattern-matching expression with a default handler which will be called if no previous .with() clause match the input value, and returns the result.

Signature

function otherwise(defaultHandler: (value: TInput) => TOutput): TOutput;

Arguments

  • defaultHandler: (value: TInput) => TOutput
    • Required
    • Function called if no pattern matched the input value.
    • Think of it as the default: case of switch statements.
    • All handlers on a single match case must return values of the same type, TOutput.

.run

match(...)
  .with(...)
  .run()

returns the result of the pattern-matching expression, or throws if no pattern matched the input. .run() is similar to .exhaustive(), but is unsafe because exhaustiveness is not checked at compile time, so you have no guarantees that all cases are indeed covered. Use at your own risks.

Signature

function run(): TOutput;

isMatching

if (isMatching(pattern, value))  {
  ...
}

isMatching is a type guard function which checks if a pattern matches a given value. It is curried, which means it can be used in two ways.

With a single argument:

import { isMatching, P } from 'ts-pattern';

const isBlogPost = isMatching({
  type: 'blogpost',
  title: P.string,
  description: P.string,
});

if (isBlogPost(value)) {
  // value: { type: 'blogpost', title: string, description: string }
}

With two arguments:

const blogPostPattern = {
  type: 'blogpost',
  title: P.string,
  description: P.string,
} as const;

if (isMatching(blogPostPattern, value)) {
  // value: { type: 'blogpost', title: string, description: string }
}

Signature

export function isMatching<p extends Pattern<any>>(
  pattern: p
): (value: any) => value is InvertPattern<p>;
export function isMatching<p extends Pattern<any>>(
  pattern: p,
  value: any
): value is InvertPattern<p>;

Arguments

  • pattern: Pattern<any>
    • Required
    • The pattern a value should match.
  • value?: any
    • Optional
    • if a value is given as second argument, isMatching will return a boolean telling us whether the pattern matches the value or not.
    • if we only give the pattern to the function, isMatching will return another type guard function taking a value and returning a boolean which tells us whether the pattern matches the value or not.

Patterns

A pattern is a description of the expected shape of your input value.

Patterns can be regular JavaScript values ("some string", 10, true, ...), data structures (objects, arrays, ...), wildcards (P._, P.string, P.number, ...), or special matcher functions (P.not, P.when, P.select, ...).

All wildcards and matcher functions can be imported either as Pattern or as P from the ts-pattern module.

import { match, Pattern } from 'ts-pattern';

const toString = (value: unknown): string =>
  match(value)
    .with(Pattern.string, (str) => str)
    .with(Pattern.number, (num) => num.toFixed(2))
    .with(Pattern.boolean, (bool) => `${bool}`)
    .otherwise(() => 'Unknown');

Or

import { match, P } from 'ts-pattern';

const toString = (value: unknown): string =>
  match(value)
    .with(P.string, (str) => str)
    .with(P.number, (num) => num.toFixed(2))
    .with(P.boolean, (bool) => `${bool}`)
    .otherwise(() => 'Unknown');

If your input isn't typed, (if it's a any or a unknown), you are free to use any possible pattern. Your handler will infer the input type from the shape of your pattern.

Literals

Literals are primitive JavaScript values, like numbers, strings, booleans, bigints, symbols, null, undefined, or NaN.

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'

Objects

Patterns can be objects containing sub-patterns. An object pattern will match If and only if the input value is an object, contains all properties the pattern defines and each property matches the 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'

Tuples (arrays)

In TypeScript, Tuples are arrays with a fixed number of elements that can be of different types. You can pattern-match on tuples using a tuple pattern. A tuple pattern will match 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

Wildcards

P._ wildcard

The P._ pattern will match any value. You can also use P.any, which is an alias to P._.

import { match, P } from 'ts-pattern';

const input = 'hello';

const output = match(input)
  .with(P._, () => 'It will always match')
  // OR
  .with(P.any, () => 'It will always match')
  .otherwise(() => 'This string will never be used');

console.log(output);
// => 'It will always match'

P.string wildcard

The P.string pattern will match any value of type string.

import { match, P } from 'ts-pattern';

const input = 'hello';

const output = match(input)
  .with('bonjour', () => 'Wonβ€˜t match')
  .with(P.string, () => 'it is a string!')
  .exhaustive();

console.log(output);
// => 'it is a string!'

P.number wildcard

The P.number pattern will match any value of type number.

import { match, P } from 'ts-pattern';

const input = 2;

const output = match<number | string>(input)
  .with(P.string, () => 'it is a string!')
  .with(P.number, () => 'it is a number!')
  .exhaustive();

console.log(output);
// => 'it is a number!'

P.boolean wildcard

The P.boolean pattern will match any value of type boolean.

import { match, P } from 'ts-pattern';

const input = true;

const output = match<number | string | boolean>(input)
  .with(P.string, () => 'it is a string!')
  .with(P.number, () => 'it is a number!')
  .with(P.boolean, () => 'it is a boolean!')
  .exhaustive();

console.log(output);
// => 'it is a boolean!'

P.nullish wildcard

The P.nullish pattern will match any value of type null or undefined.

Even though null and undefined can be used as literal patterns, sometimes they appear in a union together (e.g. null | undefined | string) and you may want to treat them as equivalent using P.nullish.

import { match, P } from 'ts-pattern';

const input = null;

const output = match<number | null | undefined>(input)
  .with(P.number, () => 'it is a number!')
  .with(P.nullish, () => 'it is either null or undefined!')
  .exhaustive();

console.log(output);
// => 'it is either null or undefined!'

P.nonNullable wildcard

The P.nonNullable pattern will match any value except null or undefined.

import { match, P } from 'ts-pattern';

const input = null;

const output = match<number | null | undefined>(input)
  .with(P.nonNullable, () => 'it is a number!')
  .otherwise(() => 'it is either null or undefined!');

console.log(output);
// => 'it is either null or undefined!'

P.bigint wildcard

The P.bigint pattern will match any value of type bigint.

import { match, P } from 'ts-pattern';

const input = 20000000n;

const output = match<bigint | null>(input)
  .with(P.bigint, () => 'it is a bigint!')
  .otherwise(() => '?');

console.log(output);
// => 'it is a bigint!'

P.symbol wildcard

The P.symbol pattern will match any value of type symbol.

import { match, P } from 'ts-pattern';

const input = Symbol('some symbol');

const output = match<symbol | null>(input)
  .with(P.symbol, () => 'it is a symbol!')
  .otherwise(() => '?');

console.log(output);
// => 'it is a symbol!'

P.array patterns

To match on arrays of unknown size, you can use P.array(subpattern). It takes a sub-pattern, and will match if all elements in the input array match this sub-pattern.

import { match, P } from 'ts-pattern';

type Input = { title: string; content: string }[];

let input: Input = [
  { title: 'Hello world!', content: 'This is a very interesting content' },
  { title: 'Bonjour!', content: 'This is a very interesting content too' },
];

const output = match(input)
  .with(
    P.array({ title: P.string, content: P.string }),
    (posts) => 'a list of posts!'
  )
  .otherwise(() => 'something else');

console.log(output);
// => 'a list of posts!'

Matching variadic tuples with P.array

In TypeScript, Variadic Tuple Types are array types created with the ... spread operator, like [string, ...string[]], [number, ...boolean[], string] etc. You can match against variadic tuple types using array literals containing ...P.array(subpattern):

import { match, P } from 'ts-pattern';

type Input = (number | string)[];

declare const input: Input;

const output = match(input)
  // P.array's parameter is optional
  .with([P.string, ...P.array()], (input) => input) // input: [string, ...(number | string)[]]
  .with(['print', ...P.array(P.string)], (input) => input) // input: ['print', ...string[]]
  // you can put patterns on either side of `...P.array()`:
  .with([...P.array(P.string), 'end'], (input) => input) // input: [...string[], 'end']
  .with(['start', ...P.array(P.string), 'end'], (input) => input) // input: ['start', ...string[], 'end']
  .otherwise((input) => input);

P.set patterns

To match a Set, you can use P.set(subpattern). It takes a sub-pattern, and will match if all elements inside the set match this sub-pattern.

import { match, P } from 'ts-pattern';

type Input = Set<string | number>;

const input: Input = new Set([1, 2, 3]);

const output = match(input)
  .with(P.set(1), (set) => `Set contains only 1`)
  .with(P.set(P.string), (set) => `Set contains only strings`)
  .with(P.set(P.number), (set) => `Set contains only numbers`)
  .otherwise(() => '');

console.log(output);
// => "Set contains only numbers"

P.map patterns

To match a Map, you can use P.map(keyPattern, valuePattern). It takes a subpattern to match against the key, a subpattern to match agains the value, and will match if all elements inside this map match these two sub-patterns.

import { match, P } from 'ts-pattern';

type Input = Map<string, string | number>;

const input: Input = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3],
]);

const output = match(input)
  .with(P.map(P.string, P.number), (map) => `map's type is Map<string, number>`)
  .with(P.map(P.string, P.string), (map) => `map's type is Map<string, string>`)
  .with(
    P.map(P.union('a', 'c'), P.number),
    (map) => `map's type is Map<'a' | 'c', number>`
  )
  .otherwise(() => '');

console.log(output);
// => "map's type is Map<string, number>"

P.when patterns

P.when lets you define your own logic to check if the pattern should match or not. If the predicate function given to when returns a truthy value, then the pattern will match for this input.

Note that you can narrow down the type of your input by providing a Type Guard function to P.when.

import { match, P } from 'ts-pattern';

type Input = { score: number };

const output = match({ score: 10 })
  .with(
    {
      score: P.when((score): score is 5 => score === 5),
    },
    (input) => '😐' // input is inferred as { score: 5 }
  )
  .with({ score: P.when((score) => score < 5) }, () => '😞')
  .otherwise(() => 'πŸ™‚');

console.log(output);
// => 'πŸ™‚'

P.not patterns

P.not lets you match on everything but a specific value. it's a function taking a pattern and returning the opposite pattern.

import { match, P } from 'ts-pattern';

type Input = boolean | number;

const toNumber = (input: Input) =>
  match(input)
    .with(P.not(P.boolean), (n) => n) // n: number
    .with(true, () => 1)
    .with(false, () => 0)
    .exhaustive();

console.log(toNumber(2));
// => 2
console.log(toNumber(true));
// => 1

P.select patterns

P.select lets you pick a piece of your input data-structure and injects it in your handler function.

It's especially useful when pattern matching on deep data structure to avoid the hassle of destructuring it in the handler function.

Selections can be either named (with P.select('someName')) or anonymous (with P.select()).

You can have only one anonymous selection by pattern, and the selected value will be directly inject in your handler as first argument:

import { match, P } from 'ts-pattern';

type Input =
  | { type: 'post'; user: { name: string } }
  | { ... };

const input: Input = { type: 'post', user: { name: 'Gabriel' } }

const output = match(input)
    .with(
      { type: 'post', user: { name: P.select() } },
      username => username // username: string
    )
    .otherwise(() => 'anonymous');

console.log(output);
// => 'Gabriel'

If you need to select several things inside your input data structure, you can name your selections by giving a string to P.select(<name>). Each selection will be passed as first argument to your handler in an object.

import { match, P } from 'ts-pattern';

type Input =
  | { type: 'post'; user: { name: string }, content: string }
  | { ... };

const input: Input = { type: 'post', user: { name: 'Gabriel' }, content: 'Hello!' }

const output = match(input)
    .with(
      { type: 'post', user: { name: P.select('name') }, content: P.select('body') },
      ({ name, body }) => `${name} wrote "${body}"`
    )
    .otherwise(() => '');

console.log(output);
// => 'Gabriel wrote "Hello!"'

You can also pass a sub-pattern to P.select if you want it to only select values which match this sub-pattern:

type User = { age: number; name: string };
type Post = { body: string };
type Input = { author: User; content: Post };

declare const input: Input;

const output = match(input)
  .with(
    {
      author: P.select({ age: P.number.gt(18) }),
    },
    (author) => author // author: User
  )
  .with(
    {
      author: P.select('author', { age: P.number.gt(18) }),
      content: P.select(),
    },
    ({ author, content }) => author // author: User, content: Post
  )
  .otherwise(() => 'anonymous');

P.optional patterns

P.optional(subpattern) lets you annotate a key in an object pattern as being optional, but if it is defined it should match a given sub-pattern.

import { match, P } from 'ts-pattern';

type Input = { key?: string | number };

const output = match(input)
  .with({ key: P.optional(P.string) }, (a) => {
    return a.key; // string | undefined
  })
  .with({ key: P.optional(P.number) }, (a) => {
    return a.key; // number | undefined
  })
  .exhaustive();

P.instanceOf patterns

The P.instanceOf function lets you build a pattern to check if a value is an instance of a class:

import { match, P } from 'ts-pattern';

class A {
  a = 'a';
}
class B {
  b = 'b';
}

type Input = { value: A | B };

const input: Input = { value: new A() };

const output = match(input)
  .with({ value: P.instanceOf(A) }, (a) => {
    return 'instance of A!';
  })
  .with({ value: P.instanceOf(B) }, (b) => {
    return 'instance of B!';
  })
  .exhaustive();

console.log(output);
// => 'instance of A!'

P.union patterns

P.union(...subpatterns) lets you test several patterns and will match if one of these patterns do. It's particularly handy when you want to handle some cases of a union type in the same code branch:

import { match, P } from 'ts-pattern';

type Input =
  | { type: 'user'; name: string }
  | { type: 'org'; name: string }
  | { type: 'text'; content: string }
  | { type: 'img'; src: string };

declare const input: Input;

const output = match(input)
  .with({ type: P.union('user', 'org') }, (userOrOrg) => {
    // userOrOrg: User | Org
    return userOrOrg.name;
  })
  .otherwise(() => '');

P.intersection patterns

P.intersection(...subpatterns) lets you ensure that the input matches all sub-patterns passed as parameters.

class A {
  constructor(public foo: 'bar' | 'baz') {}
}

class B {
  constructor(public str: string) {}
}

type Input = { prop: A | B };

declare const input: Input;

const output = match(input)
  .with(
    { prop: P.intersection(P.instanceOf(A), { foo: 'bar' }) },
    ({ prop }) => prop.foo // prop: A & { foo: 'bar' }
  )
  .with(
    { prop: P.intersection(P.instanceOf(A), { foo: 'baz' }) },
    ({ prop }) => prop.foo // prop: A & { foo: 'baz' }
  )
  .otherwise(() => '');

P.string predicates

P.string has a number of methods to help you match on specific strings.

P.string.startsWith

P.string.startsWith(str) matches strings that start with the provided string.

const fn = (input: string) =>
  match(input)
    .with(P.string.startsWith('TS'), () => 'πŸŽ‰')
    .otherwise(() => '❌');

console.log(fn('TS-Pattern')); // logs 'πŸŽ‰'

P.string.endsWith

P.string.endsWith(str) matches strings that end with the provided string.

const fn = (input: string) =>
  match(input)
    .with(P.string.endsWith('!'), () => 'πŸŽ‰')
    .otherwise(() => '❌');

console.log(fn('Hola!')); // logs 'πŸŽ‰'

P.string.minLength

P.string.minLength(min) matches strings with at least min characters.

const fn = (input: string) =>
  match(input)
    .with(P.string.minLength(2), () => 'πŸŽ‰')
    .otherwise(() => '❌');

console.log(fn('two')); // logs 'πŸŽ‰'

P.string.maxLength

P.string.maxLength(max) matches strings with at most max characters.

const fn = (input: string) =>
  match(input)
    .with(P.string.maxLength(5), () => 'πŸŽ‰')
    .otherwise(() => 'too long');

console.log(fn('is this too long?')); // logs 'too long'

P.string.includes

P.string.includes(str) matches strings that contain the provided substring.

const fn = (input: string) =>
  match(input)
    .with(P.string.includes('!'), () => 'βœ…')
    .otherwise(() => '❌');

console.log(fn('Good job! πŸŽ‰')); // logs 'βœ…'

P.string.regex

P.string.regex(RegExp) matches strings if they match the provided regular expression.

const fn = (input: string) =>
  match(input)
    .with(P.string.regex(/^[a-z]+$/), () => 'single word')
    .otherwise(() => 'other strings');

console.log(fn('gabriel')); // logs 'single word'

P.number and P.bigint predicates

P.number and P.bigint have several of methods to help you match on specific numbers and bigints.

P.number.between

P.number.between(min, max) matches numbers between min and max.

const fn = (input: number) =>
  match(input)
    .with(P.number.between(1, 5), () => 'βœ…')
    .otherwise(() => '❌');

console.log(fn(3), fn(1), fn(5), fn(7)); // logs 'βœ… βœ… βœ… ❌'

P.number.lt

P.number.lt(max) matches numbers smaller than max.

const fn = (input: number) =>
  match(input)
    .with(P.number.lt(7), () => 'βœ…')
    .otherwise(() => '❌');

console.log(fn(2), fn(7)); // logs 'βœ… ❌'

P.number.gt

P.number.gt(min) matches numbers greater than min.

const fn = (input: number) =>
  match(input)
    .with(P.number.gt(7), () => 'βœ…')
    .otherwise(() => '❌');

console.log(fn(12), fn(7)); // logs 'βœ… ❌'

P.number.lte

P.number.lte(max) matches numbers smaller than or equal to max.

const fn = (input: number) =>
  match(input)
    .with(P.number.lte(7), () => 'βœ…')
    .otherwise(() => '❌');

console.log(fn(7), fn(12)); // logs 'βœ… ❌'

P.number.gte

P.number.gte(min) matches numbers greater than or equal to min.

const fn = (input: number) =>
  match(input)
    .with(P.number.gte(7), () => 'βœ…')
    .otherwise(() => '❌');

console.log(fn(7), fn(2)); // logs 'βœ… ❌'

P.number.int

P.number.int() matches integers.

const fn = (input: number) =>
  match(input)
    .with(P.number.int(), () => 'βœ…')
    .otherwise(() => '❌');

console.log(fn(12), fn(-3.141592)); // logs 'βœ… ❌'

P.number.finite

P.number.finite() matches all numbers except Infinity and -Infinity.

const fn = (input: number) =>
  match(input)
    .with(P.number.finite(), () => 'βœ…')
    .otherwise(() => '❌');

console.log(fn(-3.141592), fn(Infinity)); // logs 'βœ… ❌'

P.number.positive

P.number.positive() matches positive numbers.

const fn = (input: number) =>
  match(input)
    .with(P.number.positive(), () => 'βœ…')
    .otherwise(() => '❌');

console.log(fn(7), fn(-3.141592)); // logs 'βœ… ❌'

P.number.negative

P.number.negative() matches negative numbers.

const fn = (input: number) =>
  match(input)
    .with(P.number.negative(), () => 'βœ…')
    .otherwise(() => '❌');

console.log(fn(-3.141592), fn(7)); // logs 'βœ… ❌'

Types

P.infer

P.infer<typeof somePattern> lets you infer a type of value from a type of pattern.

It's particularly useful when validating an API response.

const postPattern = {
  title: P.string,
  content: P.string,
  stars: P.number.between(1, 5).optional(),
  author: {
    firstName: P.string,
    lastName: P.string.optional(),
    followerCount: P.number,
  },
} as const;

type Post = P.infer<typeof postPattern>;

// posts: Post[]
const posts = await fetch(someUrl)
  .then((res) => res.json())
  .then((res: unknown): Post[] =>
    isMatching({ data: P.array(postPattern) }, res) ? res.data : []
  );

Although not strictly necessary, using as const after the pattern definition ensures that TS-Pattern infers the most precise types possible.

P.narrow

P.narrow<Input, typeof Pattern> will narrow the input type to only keep the set of values that are compatible with the provided pattern type.

type Input = ['a' | 'b' | 'c', 'a' | 'b' | 'c'];
const Pattern = ['a', P.union('a', 'b')] as const;

type Narrowed = P.narrow<Input, typeof Pattern>;
//     ^? ['a', 'a' | 'b']

Note that most of the time, the match and isMatching functions perform narrowing for you, and you do not need to narrow types yourself.

P.Pattern

P.Pattern<T> is the type of all possible pattern for a generic type T.

type User = { name: string; age: number };

const userPattern: Pattern<User> = {
  name: 'Alice',
};

Type inference

TS-Pattern takes advantage the most advanced features of TypeScript to perform type narrowing and accurate exhaustive matching, even when matching on complex data-structures.

Here are some examples of TS-Pattern's type inference features.

Type narrowing

When pattern-matching on a input containing union types, TS-Pattern will infer the most precise type possible for the argument of your handler function using the pattern you provide.

type Text = { type: 'text'; data: string };
type Img = { type: 'img'; data: { src: string; alt: string } };
type Video = { type: 'video'; data: { src: string; format: 'mp4' | 'webm' } };
type Content = Text | Img | Video;

const formatContent = (content: Content): string =>
  match(content)
    .with({ type: 'text' }, (text /* : Text */) => '<p>...</p>')
    .with({ type: 'img' }, (img /* : Img */) => '<img ... />')
    .with({ type: 'video' }, (video /* : Video */) => '<video ... />')
    .with(
      { type: 'img' },
      { type: 'video' },
      (video /* : Img | Video */) => 'img or video'
    )
    .with(
      { type: P.union('img', 'video') },
      (video /* : Img | Video */) => 'img or video'
    )
    .exhaustive();

When using P.select in a pattern, TS-Pattern will find and inject the selected value in your handler. The type of your handler's argument is inferred accordingly.

const formatContent = (content: Content): string =>
  match(content)
    .with(
      { type: 'text', data: P.select() },
      (content /* : string */) => '<p>...</p>'
    )
    .with(
      { type: 'video', data: { format: P.select() } },
      (format /* : 'mp4' | 'webm' */) => '<video ... />'
    )
    .with(
      { type: P.union('img', 'video'), data: P.select() },
      (data /* : Img['data'] | Video['data'] */) => 'img or video'
    )
    .exhaustive();

Type guard function

If you pass a type guard function to P.when, TS-Pattern will use its return type to narrow the input.

const isString = (x: unknown): x is string => typeof x === 'string';

const isNumber = (x: unknown): x is number => typeof x === 'number';

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();

Exhaustiveness checking

TS-Pattern will keep track of handled and unhandled cases of your input type. Even when pattern-matching on several union types at once, you only need to call .exhaustive() to make sure that all possible cases are correctly handled.

type Permission = 'editor' | 'viewer';
type Plan = 'basic' | 'pro';

const fn = (org: Plan, user: Permission): string =>
  match([org, user])
    .with(['basic', 'viewer'], () => {})
    .with(['basic', 'editor'], () => {})
    .with(['pro', 'viewer'], () => {})
    // Fails with `NonExhaustiveError<['pro', 'editor']>`
    // because the `['pro', 'editor']` case isn't handled.
    .exhaustive();

Inspirations

This library has been heavily inspired by this great article by Wim Jongeneel: Pattern Matching in TypeScript with Record and Wildcard Patterns. It made me realize pattern matching could be implemented in userland and we didn't have to wait for it to be added to the language itself. I'm really grateful for that πŸ™

ts-pattern's People

Contributors

colinhacks avatar dependabot[bot] avatar eltociear avatar ericcrosson avatar gvergnaud avatar hexpunk avatar jonaskello avatar juicyjusung avatar m-rutter avatar mhintz avatar momentiris avatar ndstephens avatar oguimbal avatar philer avatar shnd avatar zebruhx avatar zoontek avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ts-pattern's Issues

Wildcard and select() not working on property matching

Describe the bug
Wildcard __ and select() not working on property matching. As a workaround I use __.number

Code Sandbox with a minimal reproduction case
Result on console

https://codesandbox.io/s/nervous-turing-8cffw?file=/src/index.ts:0-937

Versions

  • TypeScript version: 4.2.3
  • ts-pattern version: 3.2.1
  • environment: node 14

Sample code

import { match, select, __ } from "ts-pattern";

type Id = { teamId: number } | { storeId: number };

const selectedId: Id = { teamId: 1 };

// Bug with wildcard
const bugWildcard = match<Id>(selectedId)
  .with({ storeId: __ }, () => "storeId")
  .with({ teamId: __ }, () => "teamId")
  .exhaustive(); // Return storeId instead of teamId

console.log(
  "WILDCARD BUG",
  "match result:",
  bugWildcard,
  "value to match:",
  selectedId
);

// Bug with select
const value = match<Id>(selectedId)
  .with({ storeId: select() }, (storeId) => storeId)
  .with({ teamId: select() }, (teamId) => teamId)
  .exhaustive(); // Return nothing

console.log(
  "SELECT BUG",
  "match result:",
  value,
  "value to match:",
  selectedId
);

// Working with __.number
const number = match<Id>(selectedId)
  .with({ storeId: __.number }, () => "storeId")
  .with({ teamId: __.number }, () => "teamId")
  .exhaustive();

console.log("WORKING", "match result:", number, "value to match:", selectedId);

Support Angular by making an ES module

Problem

When importing ts-pattern in a modern Angular app (tested on 12+), angular complains that:

Warning: <module> depends on 'ts-pattern'. CommonJS or AMD dependencies can cause optimization bailouts.
For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies

Desired Solution

Create an es-module version of this library.

Alternatives

I'm aware of no alternatives.

combining branches

Interesting library. I've been using this library: https://paarthenon.github.io/variant/ So my feedback comes from my experience with using that. One of its nice features is that you can combine branches: https://paarthenon.github.io/variant/docs/use/matching#-matcher

For example:

type FooBarBaz = {type: "foo"} | {type: "baz"} | {type: "bar"}
declare const foobarbaz: FooBarBaz;

swtich (foobarbaz.type) {
  case "foo":
  case "bar":
    // handle both foo and bar in a single branch
  case "baz: 
    // handle baz in its own branch 
}

// variant:

matcher(foobarbaz)
  .when(["foo", "bar"], (foobar) => {})
  .when({baz: (baz) => {} })
  .complete()

Matching a Narrowed Generic

Describe the bug
Matching a function parameter which has been narrowed to a specific string fails to type-check. If I predefine a type with exactly one value, it can be matched. Perhaps I'm misunderstanding how narrowing works?

// Pre-defining the type works :)
type A = "A";
function specific(tryType: A) {
  match(tryType)
    .with(tryType, () => console.log("This should match :)"))
    .exhaustive();
}

// But matching an generic type fails :(
function generic<TRY extends string>(tryType: TRY) {
  match(tryType)
    .with(tryType, () => console.log("This should match :)"))
    .exhaustive();

  // Even though TRY can only have one value 
  //  vvvvv (Does not type-check)
  const t: TRY = "this fails, because TRY was narrowed to one value";
}

Code Sandbox with a minimal reproduction case
https://codesandbox.io/s/boring-dewdney-pnxm8?file=/src/index.ts

Versions

  • TypeScript version: 4.4.3
  • ts-pattern version: 3.2.5
  • environment: node version

Thanks for your attention, and all the time you put into this project!

readme typo

Hello,
first, nice work on the lib πŸ‘

Okay, so I've noticed a little mismatch in your README (inside the select patterns section):

const output = match<Input>(input)
    .with(
      { type: 'post', user: { name: select('username') } },
      (_, { username }) => username // username: string πŸ‘ˆ, it should be ({ username}, _ ) => username
    )
    .otherwise(() => 'anonymous');

And also, the select codesandbox example leads to a codesandbox where no use of select is made ^^

Wrong type with when guard and generics

Hi πŸ‘‹

As I try to create compatibility between this library and ts-belt, I noticed an issue with when guards when they use generics.

import { match, when } from "ts-pattern";

type Nullish = null | undefined;

const someGuard = <T>(option: T | Nullish): option is T => option != null;
const noneGuard = <T>(option: T | Nullish): option is Nullish => option == null;

const __Some = when(someGuard);
const __None = when(noneGuard);

const array = Array<string>(Math.floor(Math.random() * 10)).fill("foo");
const item = array[5];

if (someGuard(item)) {
  console.log(item); // type of item is string
}
if (noneGuard(item)) {
  console.log(item); // type of item is undefined
}

match(item)
  .with(__Some, item => { /* type of item is string | undefined, since ts-pattern struggles with generic type guards */ })
  .with(__None, item => { /* type of item is undefined */ })
  .exhaustive();

It works without generic:

import { match, when } from "ts-pattern";

type Nullish = null | undefined;

const someStringGuard = (option: string | Nullish): option is string => option != null;
const noneStringGuard = (option: string | Nullish): option is Nullish => option == null;

const __Some = when(someStringGuard);
const __None = when(noneStringGuard);

const array = Array<string>(Math.floor(Math.random() * 10)).fill("foo");
const item = array[5];

if (someGuard(item)) {
  console.log(item); // type of item is string
}
if (noneGuard(item)) {
  console.log(item); // type of item is undefined
}

match(item)
  .with(__Some, item => { /* type of item is string */})
  .with(__None, item => { /* type of item is undefined */ })
  .exhaustive();

Is there a way to achieve this (a generic guard pattern)?

Deno support

Is your feature request related to a problem? Please describe.
It would be ideal to use ts-pattern on Deno via an official third party module.

Describe the solution you'd like
Ideally a Deno module could be generated via Github Actions using Denoify or a find and replace regex on import statements to include the .ts file extension which is required with Deno. (similar to what zod does)

Describe alternatives you've considered
A fork could be maintained with Deno support and have a module on the Deno third party registry but would add complexity.

Additional context
N/A

Add __.valued wildcard

Hi! Thanks for this amazing library!

I often use {x: not(__.nullish)} pattern so I wonder can we add __.valued wildcard?

Which would look like this

const isValued = <T>(val?: T | null | undefined): val is T => val !== undefined && val !== null;

Happy to create PR, if you approve.

Issue: Type instantiation is excessively deep and possibly infinite

Describe the bug

I'm getting the following error:

Type instantiation is excessively deep and possibly infinite.

when doing the following pattern.

Code Sandbox with a minimal reproduction case

You can find it here, though it does not shows the error I'm getting

here's the match pattern:

// error TS2589: Type instantiation is excessively deep and possibly infinite.
const result = match(data)
.with(
    { foo: undefined, bar: { dataList: [ { param2: { type: "thing" }, param1: select() } ] }},
    (param1) => param1
)
.with(
    { bar: { dataList: [ { param2: { type: "thing" }, param1: select() } ] }},
    (param1) => param1
)
.otherwise(() => null)

If I simplify the first with pattern into:

    { foo: undefined, bar: { dataList: [ { param2: __, param1: select() } ] }},

the issue goes away, though my selection is wrong.

If I add:

// @ts-ignore TS2589

the compilation works fine.

Versions

  • TypeScript version: 4.2.3
  • ts-pattern version: 3.1.1
  • environment:
    • lerna: 3.22.1
    • node: 5.12.0

thank you for your project :)

Optional properties on narrowed type missing

Came across what I think is a bug when it comes to the narrowed types with only additional optional properties than what you are matching on. A work around is you include the additional property as part of the match in some way as I did in the second branch.

Version 2.4.0, tsc 4.2.3

import {match, not} from 'ts-pattern';

type Foo = 
{type: "test", id?: string} |
{type: "test2", id?: string, otherProp: string} |
{type: "test3", id?: string, otherProp?: string}

declare const foo: Foo

match(foo)
.with({type: "test"}, ({id}) => {}) // Property 'id' does not exist on type '{ type: "test"; }'.(2339)
.with({type: "test", id: not(undefined)}, ({id}) => {}) // ok
.with({type: "test2"}, ({id}) => {}) // ok
.with({type: "test3"}, ({id}) => {}) // Property 'id' does not exist on type '{ type: "test"; }'.(2339)

match(foo)
.exhaustive()
.with({type: "test"}, ({id}) => {}) // Property 'id' does not exist on type '{ type: "test"; }'.(2339)
.with({type: "test", id: not(undefined)}, ({id}) => {}) // ok
.with({type: "test2"}, ({id}) => {}) // ok
.with({type: "test3"}, ({id}) => {}) // Property 'id' does not exist on type '{ type: "test"; }'.(2339)

Playground link

Type instantiation is excessively deep and possibly infinite when using select()

Describe the bug
If i use select() in a with statement Typescript complains with TS2589: Type instantiation is excessively deep and possibly infinite.

May relate to #26

Code
https://github.com/nidomiro/ts-tools/blob/4d5fe22e5a1a32d859491da8af050c1c33834c34/packages/config-helper/src/lib/config-default-impl.ts#L80

match(transformedValue)
	.with({ error: NotConvertable }, () =>
		err({
			errorType: NotConvertable,
			propertyPath,
			inputValue: value,
		} as SchemaError),
	)
	.with({ value: select() }, (value) => ok(value))
	.exhaustive()

If I remove the line .with({ value: select() }, (value) => ok(value)) this TS-error goes away.

Versions

  • TypeScript version: 4.3.5
  • ts-pattern version: 3.3.4
  • environment: node 16

Optional (1 to n) wildcard for tuples

I have a complex scenario.

Is your feature request related to a problem? Please describe.

While pattern matching a tuple with wildcards I need to define all possible scenarios although I only care about the 1st element in the tuple.

I built a complex type to show it

enum Example1 { A = 'Example1-A', B = 'Example1-B', C = 'Example1-C'}
enum Example2 { A = 'Example2-A', B = 'Example2-B', C = 'Example2-C'}
enum Example3 { A = 'Example3-A', B = 'Example3-B', C = 'Example3-C', }
enum Example4 { A = 'Example4-A', B = 'Example4-B', C = 'Example4-C', D = 'Example4-D' }
enum Example5 { A = 'Example5-A', B = 'Example5-B' }

type Data = { foo: number };
type ExampleState =
  | 'nodata'
  | [Example1.A, Data]
  | [Example1.B, Example2, Example3, Data]
  | [Example1.C, Example2.A, Example3.A]
  | [Example1.C, Example2.A, Example3.B, Data]
  | [Example1.C, Example2.A, Example3.C, Data]
  | [Example1.C, Example2.B, Example3, Data]
  | [Example1.C, Example2.C, Example3.A, 'none', Data]
  | [Example1.C, Example2.C, Example3.A, Example4.A, Data]
  | [Example1.C, Example2.C, Example3.C, Example5, Data];

Then I pattern match with several __

export function getExample(input: ExampleState) {
  const foo = () => 'foo';

  return (
    match<ExampleState, string>(input)
      .with('nodata', foo)
      .with([Example1.A, __], foo)
      .with([Example1.B, __, __, __], foo)
      .with([Example1.C, __, __], foo)
      .with([Example1.C, __, __, __], foo)
      .with([Example1.C, __, __, __, __], foo)
      .exhaustive()
  );
}

Note the last 3 cases checks for the first element to be Example1.C.

Describe the solution you'd like
I'd like to define just once the case for Example1.C

Either with a new wildcard __.optional, ____ or an array ...__.array if that's lazy and possible.

So

match<ExampleState, string>(input)
      .with('nodata', foo)
      .with([Example1.A, __], foo)
      .with([Example1.B, __.oneToN], foo)
      .with([Example1.C, __.oneToN], foo)
      .exhaustive()

Describe alternatives you've considered
I tried with .when() but TS crashes.

Type instantiation is excessively deep and possibly infinite.

type IsExample1A = Extract<ExampleState, [Example1.A, ...unknown[]]>;
type IsExample1B = Extract<ExampleState, [Example1.B, ...unknown[]]>;
type IsExample1C = Extract<ExampleState, [Example1.C, ...unknown[]]>;

export function getExample(input: ExampleState) {
  const foo = () => 'foo';

  return (
    match<ExampleState, string>(input)
      .with('nodata', foo)
      .when((value): value is IsExample1A => value !== 'nodata' && value[0] === Example1.A, foo)
      .when((value): value is IsExample1B => value !== 'nodata' && value[0] === Example1.B, foo)
      .when((value): value is IsExample1C => value !== 'nodata' && value[0] === Example1.C, foo)
      .exhaustive()
  );
}

I think .when doesn't narrow down the type, does it?
In the second .when the type is still ExampleState and not Exclude<ExampleState, IsExample1A | 'nodata'>

image

Pattern match on an `Record<>`'s key.

Is your feature request related to a problem? Please describe.

I have an object which type is alike the following:

  • const type MyErrorType = Record<string, {message: string}>

the record's key is the name of a form element, which is typed like an enum, e.g. for a form with inputs "firstname, lastname, email", the type would be alike:

  • const type MyErrorType = Record<"firstname"|"lastname"|"email", {message: string}>

For a real world use case, it's the errors object returned by "react-hook-form", for which I wish to use pattern matching.

Today I can pattern match on the keys I know how to deal with, e.g.:

    match(errors) // errors: MyErrorType
      .with(
        {firstname: {message: select()}},
        {lastname: {message: select()}}, (res) => console.error(`Name error: ${res}`))

But I have no elegant solution to match the keys I'm not sure how to deal with.

Describe the solution you'd like

Something I believe would be elegant, would be to have a pattern match destructuring on the object's key, maybe something looking like:

      .with({[select("key")]: {message: select("message")}}, ({key,message}) => console.error(`${key}: ${message}`)})

Describe alternatives you've considered

Today the only solution I found is:

      .with(__, (res) => {const keys = Object.keys(res); console.error(`${keys[0]}: ${res[keys[0]]?.message ?? "is invalid"}`)})

Additional context

I have RTFM the manual, looked through the issues, but I might have missed something better than my alternative. I also did try my solution, and it was skipped straight to the otherwise().

About the use case, I'm trying to pattern match within react-hook-form's handleSubmit second argument (that contains the form errors), which type is DeepMap<T, FieldErrors> where T is the type of the form, and FieldErrors a structure containing information about an error for a form field.

P.S.: Thank you for this work, this is something I missed a lot when doing frontend dev πŸ˜‰

Return union type without type annotating `match`

The following compiles:

import { match } from "ts-pattern";

type T = string | number;

const x = match<boolean, T>(true)
  .with(true, () => "")
  .with(false, () => 0)
  .run();

Would it be possible to construct a union type along the way in the builder? I mean, so that the above code would compile without the type annotations?

I'm not sure if that's wanted in every scenario, so maybe it would need a second version of match for it. In any way, if possible, I'd say this would be a valuable thing to have.

Matching any value in an Array pattern

Is your feature request related to a problem? Please describe.

I'm trying to match a pattern within an array:

const foobar = [
  {
    bar: 1,
    test: true
  },
  {
    bar: 2,
    test: true
  }
]

and the pattern is:

match(foobar)
  .with([{bar: select(), test: true}])
  .otherwise(() => "fail")

when foobar.foo.test is all true, the output is [1, 2]
if there's at least one false, the output is fail.

So the pattern, when applied on a list, is matching when all elements of the list match, and does not match when all the element don't. If it was an array method, it's behaving like an Array.prototype.every.

Describe the solution you'd like

Naively, when I wrote the code, I expected it to behave like an Array.prototype.filter, meaning that given:

const foobar = [
  {
    bar: 1,
    test: true
  },
  {
    bar: 2,
    test: false
  },
  {
    bar: 3,
    test: true
  }
]

The output I naively expected was [1, 3] and not "fail". Then I went back to RTFM and saw how I was mistaken. But still, I believe that behaviour would be desirable.

Describe alternatives you've considered

The solution to that would be to chain/nest the match() calls, or go back to the good old Array.prototype.filter.

I went for a solution alike:

match(foobar)
  .with(
    [{bar: select('bars'), test: select('tests')}],
    ({bars, tests}) => bars.filter((, i) => tests[i]).map(({bars}) => bars)
  )
  .otherwise(() => "fail")

Maybe a decent (and backward compatible) syntax could be adding an any guard like:

match(foobar)
  .with([{bar: select(), test: any(v => !value)}])

that would work alike the when() guard function, but selecting items of a list instead of matching them all.
(It could be also called filter or some to sound like JS functions).

Additional context

I'm discovering this module, and I'm excited about it (years of ocaml in the past, and now working in elixir in parallel made me addicted to pattern matching πŸ˜„ ), so I hope you don't mind me shooting issues and share my thoughts as a newcomer.

I made a little MCVE: https://codesandbox.io/s/ts-pattern-list-match-2m9h9

Do you think this would be implementable (the way I'm expecting, or another way)?

Incorrect type of handler function when matching an array with select()

I'm doing some simple route matching. Basically I'm splitting a path into an array of strings and extracting some parameters. Example:

const matchPath = (path: string) =>
  match(path.split("/"))
    .with(["users", select()], (name: string) => `username: ${name}`)  // error: Type 'string[]' is not assignable to type 'string'
    .run()

console.log(matchPath("users/bob"))  // works as intended, logging "username: bob"

In this example, the handler on the users route is supposed to take a string as the selected name parameter, however tsc insists that it's actually receiving string[]. It works fine at run time, only the type checker is complaining.

Code Sandbox with a minimal reproduction case
https://codesandbox.io/s/infallible-hill-igwocg?file=/src/index.ts
Note that this works as intended, only type checking fails – It looks like code sandbox does not show typing errors.

Versions

  • TypeScript version: 4.5.5
  • ts-pattern version: 3.3.5

matched variable type causes JavaScript heap out of memory

Describe the bug

I just spent 2 days trying to figure out this issue, as it broke the production build.
Basically, that caused the node VM to crash with the magnificent error below.

After hours of code bisection, I finally found what the issue was:

I'm using react-hook-form and I pattern match its errors within the onInvalid callback.

Here's a code template:

  const onInvalid: SubmitErrorHandler<MyFormType> = (errors: DeepMap<MyFormType, FieldError>) => {
    match(errors)
      .with(
        { field1: { message: __.string } },
        () => console.error(`custom error 1")
      )
      .with(
        { field2: { message: __.string } },
        () => console.error(`custom error 2")
      )
      // ... many other matches
      .otherwise(() => console.error("Fallback error"));
  };

Which caused:

$ yarn run react-scripts --max_old_space_size=8192 build
Creating an optimized production build...

<--- Last few GCs --->

[156180:0x5572b02f1420]   233071 ms: Mark-sweep 8058.8 (8229.4) -> 8040.5 (8227.8) MB, 10881.9 / 0.1 ms  (average mu = 0.129, current mu = 0.006) allocation failure scavenge might n
ot succeed
[156180:0x5572b02f1420]   245346 ms: Mark-sweep 8061.0 (8231.9) -> 8044.8 (8231.2) MB, 12243.4 / 0.1 ms  (average mu = 0.067, current mu = 0.003) allocation failure scavenge might n
ot succeed


<--- JS stacktrace --->

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
 1: 0x5572ad861041 node::Abort() [/usr/bin/node]
 2: 0x5572ad7767ba node::FatalError(char const*, char const*) [/usr/bin/node]
 3: 0x5572ada3a612 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/usr/bin/node]
 4: 0x5572ada3a878 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/usr/bin/node]
 5: 0x5572adbfa4f6  [/usr/bin/node]
 6: 0x5572adbfa64c  [/usr/bin/node]
 7: 0x5572adc0850f v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/usr/bin/node]
 8: 0x5572adc08d47 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/usr/bin/node]
 9: 0x5572adc0c3ec v8::internal::Heap::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/
bin/node]
10: 0x5572adc0c454 v8::internal::Heap::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr
/bin/node]
11: 0x5572adbd15fb v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationType, v8::internal::AllocationOrigin) [/usr/bin/node]
12: 0x5572adf184da v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [/usr/bin/node]
13: 0x5572ae272059  [/usr/bin/node]
error Command failed with exit code 1.

The really evil thing about that is that when ran through yarn+lerna, this error fails silently, lerna returning 0 to the shell. So that made the build only 10% of the files, pushing into production an incomplete build.

Resolution

I removed the whole code block, and πŸŽ‰ it worked. So then I changed the type of the errors parameter, from:

errors: DeepMap<MyFormType, FieldError>

into:

errors: Record<string, Record<string, string>|Array<Record<string, string>>>

And it worked out fine, no more issue.

  • DeepMap is from RHF and defined here.
  • FieldError is from RHF and defined here
  • MyFormType is GraphQL type with at most 3 levels of nesting.

What's tough is that it hits the ceiling without any warning or proper error.
And the compilation can take several minutes before breaking.

Versions

  • TypeScript version: 4.2.3
  • ts-pattern version: 3.1.1
  • environment: node v15.12.0

`__.empty/none` pattern

Is your feature request related to a problem? Please describe.
Sometimes something can be a union of null | undefined | T. This comes up quite often in graphql projects where null and undefined are basically interchangable. If you use something like graphql-codgen then the codegen interfaces and types will be littered with stuff like this:

type Maybe<T> = null | T;

type SomeGraphqlType = {
  __typename: "SomeGraphqlType",
  id: Scalars["ID"],
  foo?: Maybe<SomeOtherGraphqlType | YetAnotherGraphqlType> 
}

type SomeOtherGraphqlType = {
  __typename: "SomeOtherGraphqlType"
}

type YetAnotherGraphqlType= {
  __typename: "YetAnotherGraphqlType"
}

Sometimes these nullable unions can be quite large, and so you just want to check if its null/undefined and then maybe later match on that union in some other code branch.

Right now you can do match(x).with(not(undefined), not(null), () => {}), which works find, but sometimes that can be a bit laborious if what you want to match on in a little more complex than the above example, such as when you want to match against multiple nullable fields at lots of different depths.

Describe the solution you'd like
A __.empty or __.none pattern that matches null | undefined. Allowing a user to write this:

match(x).with({y: not(__.empty)}, ({y}) => {
  // y is not `null | undefined`
});

Describe alternatives you've considered
Alternatives are:
Use a when with a type guard that takes a null | undefined | T and returns x is T
Provide multiple patterns, one of which is not(undefined) and the other which is not(null).

Additional context
https://www.graphql-code-generator.com/

Empty arrays, without `when`?

This fails by returning Array of cats, not Empty array. The logic might make sense, the array is indeed an empty array of cats. However, it would be useful to test for empty arrays - without receding to when. Is there a way to do that?

const isEmptyArray = match({keys: []})
	.with({keys: ["cat"]}, () => "Array of cats")
	.otherwise(() => "Empty array")

isEmptyArray.should.equal("Empty array")

Array element matching bug on empty arrays

Hi !

Describe the bug

import { match, __, not } from 'ts-pattern';

console.log(match([]) // Logs πŸ‘‰ first
  .with([__], () => 'first')
  .otherwise(() => 'default'));

console.log(match([]) // Logs πŸ‘‰ first
  .with([not(__.nullish)], () => 'first')
  .otherwise(() => 'default'));

Even if the first is a bit ambiguous, I would at least expect the second one NOT to print "first"...

Code Sandbox with a minimal reproduction case

https://codesandbox.io/s/icy-glade-3ty9l?file=/src/index.ts

Versions

  • TypeScript version: 4.3.4
  • ts-pattern version: 3.2.5
  • environment: any

Regex Support Without When

Is your feature request related to a problem? Please describe.

This is a quality-of-life improvement for developers using this library. There are existing alternatives, but this would be a nice improvement.

Describe the solution you'd like

import { match } from 'ts-pattern';

const regex = /\${.*}/;
const prop = '${abc}';
const value = '1.0.0';

const msg = match(prop)
	.with(regex, () => 'Regex')
	.otherwise(() => 'Value');

In that example, a regex I have created would be used in pattern matching. This example does not work, a regex is not an acceptable pattern when passed to with(). Now this can work using when:

import {match, when } from 'ts-pattern';

const regex = /\${.*}/;
const prop = '${abc}';
const value = '1.0.0';

const msg = match(prop)
	.with(when<string>((_) => regex.test(_)), () => 'Regex')
	.otherwise(() => 'Value');

However, it would be nice if we had the ability to apply a regex test directly.

Rather than accepting the regex as a value to the pattern argument, a simple regex wrapper could be provided. This might be easier to implement in terms of its compatibility with ts-pattern's existing type system.

import {match, when, reg} from 'ts-pattern';

const regex = /\${.*}/;
const prop = '${abc}';
const value = '1.0.0';

const msg = match(prop)
	.with(reg(regex), () => 'Regex')
	.otherwise(() => 'Value');

Anyway, just an idea I have.

Describe alternatives you've considered

I have described alternatives above.

Additional context

Just an idea for an enhancement. Thanks.

Exhaustive check fails with tuple of tuples

Describe the bug
While matching against a tuple that has a tuple and the inner tuple has variable number of elements, the exhaustive check fails.

Code Sandbox with a minimal reproduction case
https://codesandbox.io/s/romantic-architecture-39wbm

However this works fine:
https://codesandbox.io/s/aged-field-vrldb

So it can be seen that if in a tuple we have a tuple which size varies then it fails the exhaustive check.

Versions

  • TypeScript version: 4.5.4
  • ts-pattern version: 3.3.4
  • environment: not related, it fails compilation

__.nullish matches null but does not match when value does not exist

$ cat script.js 
var match = require('ts-pattern').match
var __ = require('ts-pattern').__
var input = {b: 'b value'}
console.log(match(input).with({a: __.nullish}, () => 'success').otherwise(() => 'failure'))
var input2 = {a: null, b: 'b value'}
console.log(match(input2).with({a: __.nullish}, () => 'success').otherwise(() => 'failure'))
$ node script.js 
failure
success

It seems logical that both should output success

Or if this is not the intended usage of __.nullish then what's the right way to match when something doesn't exist?

ts-pattern@npm:3.3.5
typescript Version 4.5.5

Exhaustiveness check fails for object patterns with typos

Describe the bug
When pattern matching against multiple attributes in an object, ts-pattern will throw a runtime error that the exhaustive pattern matching was not able to spot at build-time if one of the attributes is misspelled.

Code Sandbox with a minimal reproduction case
https://codesandbox.io/s/elated-wilbur-h0mz2?file=/src/index.ts
(copy+pasted too):

import { match } from "ts-pattern";

type Person = {
  sex: "Male" | "Female";
  age: "Adult" | "Child";
};

function summary(person: Person): string {
  return (
    match(person)
      // Typo – "agf" should be "age"
      .with({ sex: "Female", agf: "Adult" }, () => "Woman")
      .with({ sex: "Female", age: "Child" }, () => "Girl")
      .with({ sex: "Male", age: "Adult" }, () => "Man")
      .with({ sex: "Male", age: "Child" }, () => "Boy")
      // there is no TS error
      .exhaustive()
  );
}

// yet this throws an error at runtime :(
console.log(summary({ sex: "Female", age: "Adult" }));

Versions

  • TypeScript version: 4.3.5
  • ts-pattern version: 3.2.4
  • environment: browser + version / node version / deno version

Thanks – this library is great!

4.3.2 regression involving branded types

Describe the bug
I'm not sure if this is a typescript or a ts-pattern bug, but I thought you might be best placed to triage.

I tried upgrading to typescript 4.3 the other day, and I hit some odd type errors in ts-pattern usages. I've managed to narrow and simplify it down to this example which involves branded types:

import { match, when } from "ts-pattern";
export type BrandedId = string & { __brand: "brandId" };
type FooBar = { type: "foo"; value: string } | { type: "bar" };

type State = {
  fooBar: FooBar;
  fooBarId: BrandedId;
};

declare const state: State;

match(state).with(
  { fooBar: { type: "foo" }, fooBarId: when((id) => id === "") },
  ({ fooBar: tempApp }) => { // 4.2 narrowed to the "foo" variant, 4.3 is still `FooBar`
    console.log(tempApp.value); // error in 4.3.x but not 4.2.x,
  }
);

4.4-dev playground (4.3-beta does not reproduce, and 4.3 isn't an option yet)

4.2 playground example
4.3-beta playground example

Versions

  • TypeScript version: 4.3.2 and 4.4 nightly
  • ts-pattern version: 3.1.6
  • environment: not relevant

Add support for pattern matching class instances

Let's suppose we have the following types:

class UserId {
    constructor(public val: string, private _?: string) {}
}

class SellerId {
    constructor(public val: string, private _?: string) {}
}

type Ids = UserId | SellerId

I've found that for nominal typing, a class with a private property is IMO the best and simplest way to achieve it. It is quite an advantage having both constructor and type on the same place. No unique symbol branding and no type casting on the constructor functions.

Also, as long as no inheritance shenanigans happen it allows to do strict pattern matching out of the box:

const assertNever = (val: never): never => {
    throw new Error('Should never happen')
}

const processId = (id: Ids): string => {

    if (id instanceof UserId) {
        return 'this is a user id'
    } else if (id instanceof SellerId) {
        return 'this is a seller id'
    }

    return assertNever(id)
}

Sadly, I have to put an ugly assertNever at the end.

I just discovered this lib, and it would be quite nice to be able to pattern match those cases too without tedious instanceof chains.

A sample implementation would be something like:

match(id)
    .with(UserId, (res) => 'UserId detected')
    .with(SellerId, (res) => 'SellerId detected')
    .exhaustive()

How difficult would it be to implement it?

error loading library: '=' expected. TS1005

Hi, I've installed the lib and add this to my project, when I try to run a react app I get this error

/Users/Admin/Downloads/proj/...../node_modules/ts-pattern/lib/index.d.ts
TypeScript error in /Users/Admin/Downloads/proj/.../node_modules/ts-pattern/lib/index.d.ts(1,13):
'=' expected.  TS1005

  > 1 | import type { Pattern, AnonymousSelectPattern, NamedSelectPattern, GuardPattern, NotPattern, GuardFunction } from './types/Pattern';
      |             ^
    2 | import type { Match } from './types/Match';
    3 | import { __ } from './PatternType';
    4 | export declare const when: <a, b extends a = a>(predicate: GuardFunction<a, b>) => GuardPattern<a, b>;

do you know what could be the problem here?

thank you so much

String matching with regular expressions

It would be very nice to be able to match on regular expressions:

match("horse")
   .with(/.+rse/, () => "probably a horse")
   .otherwise(() => "Very likely not a horse")

Performance and usablity of `.exhaustive`

So I noticed that exhaustive has some pretty punishing compile times for any moderately complicated types it needs to match on. I've even found it hitting the "union type that is too complex to represent" limit regularly. I don't have any benchmarks. but I think you are aware of the problem as you mention it your docs. I'm not sure if exhaustive is actually usable except for the most simple cases.

If I had to guess it is because if you have a type like this:

type A = {type: "a", mode: "b" | "c" | "d"} |  {type: "b", mode: "f" | "g"}

You have to generate a union that looks like this?:

  | {type: "a", mode: "b"} 
  | {type: "a", mode: "c"} 
  | {type: "a", mode: "d"} 
  | {type: "b", mode: "f"} 
  | {type: "b", mode: "g"}   

So for example this fairly simple to understand union will completely break exhaustive:

import { Property } from "csstype";

declare const a:
  | { type: "textWithColor"; color: Property.Color }
  | { type: "textWithColorAndBackground"; color: Property.Color; backgroundColor: Property.Color };

match2(a).exhaustive(); // "union type that is too complex to represent"

playground link - takes serveral minutes on my machine to hit the limit

The reason being is that Property.Color is a string union with hundreds of variants. (this is the same kind of example that eventually lead the typescript team to abandon by default inference of template string literals types in 4.2 - microsoft/TypeScript#42416)

So this makes exhaustive pretty much unusable if you have a type with properties that are unions of any moderate size. Either because you will hit the union limit or because the compile times are too extreme to make it practical to use.

This is all fair, and I don't think I see a way around the issue and keeping the full pattern matching features of the lib.

That said, in 80-90% of cases all I want to match on is the discriminator of a union in order to narrow the types. For example this works in a simple switch and I still get some kind of exhaustiveness checks:

declare const a:
  | { type: "textWithColor"; color: Property.Color }
  | { type: "textWithColorAndBackground"; color: Property.Color; backgroundColor: Property.Color };
  
  
 const aToDescription (a: typeof a): string  => { 
   switch(a.type) {
    case "textWithColor":
       return a.color
     case "textWithColorAndBackground":
       return `${a.color} with a background of ${a.backgroundColor}`
   }
}

I'm wondering if you can offer something that still allows for some kind of exhaustiveness checks on discriminators in order to narrow types down, but compromises on pattern matching features elsewhere for the sake of compile time performance. In the back of my mind I'm thinking about this lib https://paarthenon.github.io/variant/ (mentioned in my my previous issue) because this library can do exhaustiveness checks because it only concerns itself with the discriminators of unions.

select support for array matches

Is your feature request related to a problem? Please describe.
It would be great if we could select the matched field in case of arrays too

Describe the solution you'd like
honestly I'm not sure how would this be possible with the current API but maybe if it could use select as a wrapper on the cases
for example in this case:

match([x, y])
      .with([when(not(isArray)), __], ([_]) => createNonArrayFailure(_))
      .with([when(not(isEmptyArray)), when(or(isNull, isUndefined))], ([_]) =>
        createEmptyArrayFailure(_))

it would be cool if I could do something like this

match([x, y])
      .with([select(when(not(isArray))), __], createNonArrayFailure)
      .with([when(not(isEmptyArray)), select(when(or(isNull, isUndefined)))], createEmptyArrayFailure)

or maybe

match([x, y])
      .with([when(not(isArray)), __], [select(), __], createNonArrayFailure)
      .with([when(not(isEmptyArray)), when(or(isNull, isUndefined))], [__, select()], createEmptyArrayFailure)

Add peerDependency on TypeScript ^4.0.0in ts-pattern ^2.0.0

Hi @gvergnaud,

Is TypeScript ^4.0.0 a requirement to consume ts-pattern ^2.0.0? I found this line on the v2.0.1 rc

People using earlier versions of TS will need to upgrade in order to use ts-pattern v2+.

It's not clear to me if only ts-pattern needs a TS version 4-or-greater or if consumers of ts-pattern do too. If it's the latter case, we should add a peerDependency in the package.json to codify this to npm

ts-pattern doesn't compile with typescript 4.5.0-beta

Describe the bug
It appears that ts-pattern project itself doesn't compile with the 4.5.0-beta. You get various errors around stack depth and non-assignablilty.

In my own project using ts-pattern 3.3.3, when I tried to upgrade to 4.5.0-beta I hit excessive stack depth errors on code that previously compiled in 4.4.4.

Code Sandbox with a minimal reproduction case
I haven't been able to create a simple reproduction of consuming v3.3.3 using 4.5.0-beta. I will update when I can easily reproduce in an isolated way.

Versions

  • TypeScript version: 4.5.0-beta
  • ts-pattern version: 3.3.3

Does this library work with webpack?

Using [email protected] and [email protected], I get this error:

ERROR in ./node_modules/ts-pattern/lib/index.js
Module not found: Error: Can't resolve './guards' in 'C:\work\zwiftly\source\Zwiftly.WebApp\node_modules\ts-pattern\lib'
@ ./node_modules/ts-pattern/lib/index.js 5:17-36
@ ./app/app.ts

ERROR in ./node_modules/ts-pattern/lib/index.js
Module not found: Error: Can't resolve './symbols' in 'C:\work\zwiftly\source\Zwiftly.WebApp\node_modules\ts-pattern\lib'
@ ./node_modules/ts-pattern/lib/index.js 4:16-36
@ ./app/app.ts

ERROR in ./node_modules/ts-pattern/lib/index.js
Module not found: Error: Can't resolve './wildcards' in 'C:\work\zwiftly\source\Zwiftly.WebApp\node_modules\ts-pattern\lib'
@ ./node_modules/ts-pattern/lib/index.js 10:20-42
@ ./app/app.ts

I haven't seen this in other projects where I'm using ts-pattern. I'm sure it can't be a bug, because other people would have stumbled on it before me. But I'm not really sure what to do. I've tried searching the issues here in your repo, but came up blank.

Any ideas?

My ts.config looks like this:

{
	"compilerOptions": {
		"module": "ES6",
		"removeComments": true,
		"sourceMap": true,
		"lib": [ "es2017", "dom" ],
		"strictNullChecks": false,
		"moduleResolution": "node",
		"target": "es5",
		"noResolve": false,
		"allowSyntheticDefaultImports": true,
		"baseUrl": ""
	},
	"include": [
		"app/**/*.ts",
		"typings/BackendApi/*.ts"
	],
	"exclude": [
		"app/specs/**/*.ts"
	],
}

and my webpack configuration looks like this:

// webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
	entry: './app/app.ts',
	module: {
		rules: [
			{
				test: /\.ts$/,
				use: 'ts-loader',
				exclude: /node_modules/
			}
		]
	},
	resolve: {
		extensions: ['.ts']
	},
	plugins: [
		new CleanWebpackPlugin(),
		new HtmlWebpackPlugin({
			filename: path.resolve(__dirname, './Views/Home/Index.cshtml'),
			template: './Views/Home/Index.template.cshtml',
			inject: false
		})
	],
	output: {
		filename: '[name].[contenthash].bundle.js',
		path: path.resolve(__dirname, 'js'),
		publicPath: '/'
	},
	optimization: {
		splitChunks: {
			cacheGroups: {
				vendor: {
					test: /[\\/]node_modules[\\/]/,
					name: 'vendor',
					chunks: 'all'
				}
			}
		}
	}
};

// webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
	mode: 'development',
	devtool: 'inline-source-map',
	watch: true
});

Tuples within objects not selected properly

Describe the bug

import {match, select} from "ts-pattern";

const r1 = match({$not: ["B"]})
	.with({$not: [select("term")]}, ({term}) => term)
	.run()
console.log(r1); // [ "B" ]

const r2 = match({a: [{$not: ["B"]}]})
	.with({a: [{$not: [select("term")]}]}, ({term}) => term)
	.run()
console.log(r2); // [ [ "B" ] ]

I was expecting these to both log "B". Is this correct? Thanks!

Code Sandbox with a minimal reproduction case
https://codesandbox.io/s/bitter-moon-hwnzz

Versions

  • TypeScript version: 4.5.4
  • ts-pattern version: 3.3.4
  • environment: Node.js v 14.18.1

Passthrough match accumulator

First and foremost, I'd like to say thank you for this excellent library. This is exactly what I was looking for in JS land!

This library checks out 80% of my use-cases, but there's one thing I've always wondered about...

Is your feature request related to a problem? Please describe.

Sometimes you want to traverse the object and check for some condition, but instead of returning the result immediately, you want to accumulate the results.

Describe the solution you'd like

NOTE: I don't have a good name for this, but for this FR let's go with Sink.

Consider the following examples:

const person = {
  name: 'Daniel',
  nickname: 'Fedaikin'
} as const

const card = Sink(person)
  .with({ name: 'Daniel' }, ({ name }) => `My name is ${name}`)
  .with({ nickname: 'Fedaikin' }, ({ nickname }) => `My nickname is ${nickname}`)

assert(card[0] === 'My name is Daniel')
assert(card[1] === 'My nickname is Fedaikin')

As you can see, the result of this operation is an array with returned values for all of the matches.

We can also expand it with something that will allow us to just run the side-effects on matches without pushing values into the array.

We can reuse many matchers that you already have, but one of the API's I'd love to have that's missing right now would look like this:

const card = Sink(person)
  .key('name', ({ name }) => `My name is ${name}`)
  .key('nickname', ({ nickname }) => `My nickname is ${nickname}`)

This API is exactly like Ram(b)da path, lodash/get, so nothing novel here.
This API can also be expanded to your current implementation.

Describe alternatives you've considered

Alternatives are pretty simple: I've been playing with demos on my own, and this feature can just become its own package.

Pros:

  • The purpose of this package will remain intact; it'll continue to do one thing exceptionally well
  • Initial API and typings can be significantly simpler since this way, I can do it incrementally, instead of implementing full suite of features from the get-go

Cons:

  • We will not be able to leverage matchers, typings (some of which are pretty complex), all of which I'd have to re-implement. This has a problem: if someone is using both packages, they will end up with "duplicate code" that will have ~same-ish functionality in their core
  • This project is already established, so it'll reach more users and help them

Additional context

My PoC was using classes, and I dropped it for the time being, at least until you review this issue. I am planning on helping you (if I even could) implementing this feature if you give this FR the green light.

Would it be possible to get rid of run?

Hi, I am really impressed with this library, great job!

I have just tiny cosmetic issue, that is not preventing me from using library at all but...

Would it be possible to make run() on patterns without otherwise() hidden so it doesn't hang in there like some ugly tail?

Chaining patterns with logical operators

Is your feature request related to a problem? Please describe.
consider the following class hierarchy:

abstract class N {
  a: N
  b: N
}

class A extends N {
}

class B extends N {
}

Using the instanceOf pattern, it's possible to match an exact instance of either A or B directly, or as a child at some point in the tree:

const a = new A()

const found = match<N, N>(a)
  .with(instanceOf(A), (v) => {console.log('got an A'); return v})
  .run()

const deepFind = match<N, N>(a)
  .with({a: instanceOf(A), b: instanceOf(B)}, (v) => {console.log('v is either A or B, a is an A, b is a B'); return v})
  .run()

However, there does not seem to be a way to do both of these together to narrow the types (at least, to such an extent that an IDE might be able to correctly narrow for tooltips).

Describe the solution you'd like
Some sort of and / intersect pattern, similar in concept to what not is doing: it would take multiple patterns as arguments, and each of them must pass to correctly narrow the type. This suggests a further possibility: or / union for broadening types. This is, of course, assuming that such functionality would correctly trigger the type inference functionality of TypeScript.

So, deepFind might look like:

const deepFind = match<N, N>(a)
  .with(and(instanceOf(A), {a: instanceOf(B), b: instanceOf(B)}), (v) => {console.log('got an A with {a, b} both B'); return v})

Describe alternatives you've considered
deepFind, above, could be done with a slight alteration to the class hierarchy:

abstract class N {
  type: string
  a: N
  b: N
}

class A extends N {
  type = 'A'
}

class B extends N {
  type = 'B'
}

Then, when doing a structural search, one could pattern match on tyoe and assert the instance types of the other properties:

const deepFind = match<N, N>(a)
  .with({type: 'A', a: instanceOf(B), b: instnaceOf(B)}, (v) => {console.log('an A instance with {a, b} both B'; return v})
  .run()

This works currently, but requires extra data be stored in each class instance, while also requiring more set up on each class. For this case the extra effort is trivial; for more expansive class hierarchies or larger datasets, it would be more cumbersome.

I'm aware that with can take a guard predicate as an optional second parameter, but, to my knowledge, that does not affect type narrowing. Further, I don't believe narrowing would necessarily be done on use of a when predicate, should something like the following be done:

const deepFind = match<N, N>(a)
  .when((v) => v instanceOf(A) && v.a instanceOf(A), (v) => {console.log('is either v or v.a narrowed? Is this safe? what happens if v.a undefined?'); return v})
  .run()

Additional context
My apologies if some of this seems discombobulated: most of this is written in the context of IntelliSense feedback from VS Code; some of the above might work correctly even if the editor is unable to provide proper feedback about it.

Unexpected type errors with `any` and `unknown` combined with `not` pattern

Describe the bug
I had a type like this:

type Foo = { date?: any };

I meant for this to be a {date?: Date}, but nevermind - blame codegen.

I tried to match on it with not(__.nullish), and I got some unsual errors:

match(foo).with({date: not(__.nullish)}, (foo) => {
    const date = foo.date;
})

// Property 'date' does not exist on type 'SeveralAnonymousSelectError<"You can only use a single anonymous selection (with `select()`) in your pattern. If you need to select multiple values, give them names with `select(<name>)` instead">'.(2339)

It seems trying to use not with any any or unknown values with any type of pattern produces this error. Now it doesn't make any sense to use not on any or unknown, but these errrors are quite confusing and appear unrelated to the mistake I've made. I haven't looked at it closely, but I'm guessing SeveralAnonymousSelectError is some kind of fallthrough case that this pattern is unintentionally hitting.

My expectation would be that either we would get a more relevant error (something about how negated types aren't possible or something), or date would remian any.

I'll look into it more closely myself later.

Code Sandbox with a minimal reproduction case
playground example

Versions

  • TypeScript version: 4.3.5 & 4.4.0-beta
  • ts-pattern version: 3.2.4

Typed `with` handlers are unsafe

Describe the bug
Consider this example:

const f = (x: number) => x.toLocaleString();
const g = (x: string) => x.toUpperCase();
const input = Math.random() > 0.5;
match(input).with(true, f).with(false, g).exhaustive();

This typechecks, but is guaranteed to crash at runtime (the handler functions do not accept booleans).

Code Sandbox with a minimal reproduction case
https://codesandbox.io/s/wonderful-zeh-gw5r7

Versions

  • TypeScript version: 4.4.4
  • ts-pattern version: 3.3.4
  • environment: all environments

Ability to select() tuple element as a whole

Consider an input made of tuples + enums

enum Foo { ... }
enum Bar { ... }
type State = 
  | [Foo.A, Bar.A, {age: number}]
  | [Foo.A, Bar.B, {name: string, age: number}]

Note I'm using the last tuple element for the data, when patter matching I use a wildcard on it. Then I destructure the handler selections to access data

.with([Foo.A, Bar.A, __], ([_foo, _bar, data]) => {
  // do something with data
})
.with([Foo.A, Bar.B, __], ([_foo, _bar, data]) => {
  // do something with data
})

The only workaround with select I know is to list all data's properties

.with([Foo.A, Bar.A, { age: select('age')}], (data) => {
  // ...
})
.with([Foo.A, Bar.B, { name: select('name'), age: select('age') }], (data) => {
  // ...
})

Describe the solution you'd like

It would be great to select the wildcarted element

.with([Foo.A, Bar.A, __.select], (data) => {
  // ...
})
.with([Foo.A, Bar.B, __.select], (data) => {
  // ...
})

Describe alternatives you've considered
A workaround I'm using is to move the selection outside the pattern matching, but this doesn't act as a wildcard and only works if multiple cases have same data

const selectData = {
  name: select('name'),
  age: select('age')
}

...

.with([Foo.A, Bar.A, selectData], (data) => {
  // ...
})
.with([Foo.A, Bar.B, selectData], (data) => {
  // ...
})

select() of optional fields breaks exhaustiveness

Describe the bug
When adding select() to a pattern field which type is optional ts-pattern thinks it's no longer exhaustive

Code Sandbox with a minimal reproduction case
https://codesandbox.io/s/nameless-wood-bfwi4?file=/src/index.ts

import { match, select } from "ts-pattern";

type Action =
  | { type: "one" }
  | { type: "two"; data?: string } // works ok when changing to `data: string`
  | { type: "three" };

declare const action: Action;

match<Action>(action)
  .with({ type: "one" }, () => {})
  .with({ type: "two", data: select() }, () => {})
  .with({ type: "three" }, () => {})
  .exhaustive();

Versions

  • TypeScript version: 4.3.3
  • ts-pattern version: 3.3.3

when() guard with the ability to select() if true

Is your feature request related to a problem? Please describe.

First of all, if this is already possible I'm going to apologize in advance. I've only recently discovered your library, and I think it is absolutely incredible. As a strong functional programming advocate who also works in TypeScript a lot, this is one of those missing pieces that I have been waiting for.

Describe the solution you'd like

The when() guard is extremely helpful, especially when working with types from the fp-ts library. The following example uses fp-ts/Option, which has sub-types of Some or None.

match(some)
		.with(when(O.isSome), (theSome) => console.log('Some', theSome.value))
		.with(when(O.isNone), () => console.log('None'))
		.run();

This is all well and good, but as you can see above the Some type has an additional property called "value". While there are multiple ways to select this property, one of which you see above, it would be nice to be able to use your select() function on it as well.

Based on all the documentation I've gone through so far, there appears to be no way to combine the when() and some() functions into a single expression. This is disappointing.

Describe alternatives you've considered

I have looked at multiple pattern matching libraries for TypeScript, and so far I'm liking yours most of all. This issue is not a dealbreaker for me, I'm more hoping that I've just missed something in your docs.

Additional context

I want to emphasize what a wonderful job you have done with this library. It is a truly excellent addition to the TypeScript world. I can see how much you have done, and how much you continue to actively do. Thank you for your excellent work.

Exhaustiveness checking?

Very cool library, thanks for creating it! Would it be possible to force exhaustiveness checking for union types? For example:

function getSomeString(unionType:  "foo" | "bar"): string{
     return match(unionType).with("foo", () => `A string result, but we won't check for "bar"!`).run()
}

This will not be a compile time error. If you write it as

function getSomeString(unionType:  "foo" | "bar"): string{
    if(unionType === "foo"){
        return `A string result, but we won't check for "bar"!`
    } 
}

You get TS2366: Function lacks ending return statement and return type does not include 'undefined' because TS can verify that you haven't exhausted all of the cases.

It would be really nice if this library could take advantage of that. Failure to check all cases is a really easy bug to introduce!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.