Skip to content

✨ v4.0.1 ✨

Compare
Choose a tag to compare
@gvergnaud gvergnaud released this 26 Mar 10:53
3910835

⚠️ Breaking changes

Imports

type-specific wildcard patterns have moved from __.<pattern> to a new Pattern qualified module, also exported as P by ts-pattern.

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


const toString = (value: string | number) =>
  match(value)
-   .with(__.string, (v) => v)
-   .with(__.number, (v) => `${v}`)
+   .with(Pattern.string, (v) => v)
+   .with(Pattern.number, (v) => `${v}`)
    .exhaustive();

or

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


const toString = (value: string | number) =>
  match(value)
-   .with(__.string, (v) => v)
-   .with(__.number, (v) => `${v}`)
+   .with(P.string, (v) => v)
+   .with(P.number, (v) => `${v}`)
    .exhaustive();

__

The top level __ export was moved to P._ and P.any:

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


const toString = (value: string | number) =>
  match(value)
-   .with(__, (v) => `${v}`)
+   .with(P._, (v) => `${v}`)
    // OR
+   .with(P.any, (v) => `${v}`)
    .exhaustive();

select(), not(), when()

Function to create patterns have been moved to the P module.

- import { match, select, not, when } from 'ts-pattern';
+ import { match, P } from 'ts-pattern';


const toString = (value: number) =>
  match(value)
-   .with({ prop: select() }, (v) => `${v}`)
+   .with({ prop: P.select() }, (v) => `${v}`)

-   .with({ prop: not(10) }, (v) => `${v}`)
+   .with({ prop: P.not(10) }, (v) => `${v}`)

-   .with({ prop: when((x) => x < 5) }, (v) => `${v}`)
+   .with({ prop: P.when((x) => x < 5) }, (v) => `${v}`)
    .exhaustive();

Pattern type

the Pattern type which used to be exported at the toplevel is now accessible at P.Pattern.

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

- const pattern: Pattern<number> = P.when(x => x > 2);
+ const pattern: P.Pattern<number> = P.when(x => x > 2);

list patterns

The syntax for matching on a list of elements with an unknown length has changed from [subpattern] to P.array(subpattern).

Example:

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


const parseUsers = (response: unknown) =>
  match(response)
-   .with({ data: [{ name: __.string }] }, (users) => users)
+   .with({ data: P.array({ name: P.string }) }, (users) => users)
    .otherwise(() => []);

Now [subpattern] matches arrays with 1 element in them. This is more consistent with native language features, like destructuring assignement and is overall more intuitive. This will resolve #69, #62 and #46.

NaN

The __.NaN pattern has been replaced by simply using the NaN value in the pattern:

match<number>(NaN)
-   .with(__.NaN, () => "this is not a number")
+   .with(NaN, () => "this is not a number")
    .otherwise((n) => n);

⭐️ New features ⭐️

Here is the list of all new features which have been added in TS-Pattern v4.

Arrays and unary tuples

P.array(pattern)

To match an array of elements, you can now use P.array:

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

const responsePattern = {
  data: P.array({
    id: P.string,
    post: P.array({
      title: P.string,
      content: P.string,
    }),
  }),
};

fetchSomething().then((value: unknown) =>
  match(value)
    .with(responsePattern, (value) => {
      // value: { data: { id: string, post: { title: string, content: string }[] }[] }
      return value;
    })
    .otherwise(() => {
      throw new Error('unexpected response');
    })
);

Optional object properties

P.optional(pattern)

If you want one of the keys of your pattern to be optional, you can now use P.optional(subpattern).

If you P.select() something in an optional pattern, it's type will be infered as T | undefined.

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

const doSomethingWithUser = (user: User | Org) =>
  match(user)
    .with(
      {
        type: 'user',
        detail: {
          bio: P.optional(P.string),
          socialLinks: P.optional({
            twitter: P.select(),
          }),
        },
      },
      (twitterLink, value) => {
        // twitterLink: string | undefined
        /**
         *  value.detail: {
         *      bio?: string,
         *      socialLinks?: {
         *          twitter: string
         *      }
         *  }
         **/
      }
    )
    .otherwise(() => {
      throw new Error('unexpected response');
    });

Union & intersection patterns

P.union(...patterns) and P.intersection(...patterns) combine several patterns into a single one, either by checking that one of them match the input (p.union) or all of them match it (P.intersection).

P.union(...patterns)

type Input =
  | { type: 'a'; value: string }
  | { type: 'b'; value: number }
  | {
      type: 'c';
      value:
        | { type: 'd'; value: boolean }
        | { type: 'e'; value: string[] }
        | { type: 'f'; value: number[] };
    };

const f = (input: Input) =>
  match(input)
    .with(
      { type: P.union('a', 'b') },
      // x: { type: 'a'; value: string } | { type: 'b'; value: number }
      (x) => 'branch 1'
    )
    .with(
      // P.union can take any subpattern:
      {
        type: 'c',
        value: { value: P.union(P.boolean, P.array(P.string)) },
      },
      (x) => 'branch 2' // x.value.value: boolean | string[]
    )
    .with({ type: 'c', value: { type: 'f' } }, () => 'branch 3')
    .exhaustive();

P.intersection(...patterns)

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

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

const f = (input: { prop: A | B }) =>
  match(input)
    .with(
      { prop: P.intersection(P.instanceOf(A), { foo: 'bar' }) },
      // prop: A & { foo: 'bar' }
      ({ prop }) => 'branch 1'
    )
    .with(
      { prop: P.intersection(P.instanceOf(A), { foo: 'baz' }) },
      // prop: A & { foo: 'baz' }
      ({ prop }) => 'branch 2'
    )
    .with(
      { prop: P.instanceOf(B) },
      // prop: B
      ({ prop }) => 'branch 3'
    )
    .exhaustive();

Select with sub pattern

P.select() now can take a subpattern and match only what the subpattern matches:

type Img = { type: 'img'; src: string };
type Text = { type: 'text'; content: string; length: number };
type User = { type: 'user'; username: string };
type Org = { type: 'org'; orgId: number };

const post = (input: { author: User | Org; content: Text | Img }) =>
  match(input)
    .with(
      { author: P.select({ type: 'user' }) },
      // user: User
      (user) => {}
    )
    .with(
      {
        // This also works with named selections
        author: P.select('org', { type: 'org' }),
        content: P.select('text', { type: 'text' }),
      },
      // org: Org, text: Text
      ({ org, text }) => {}
    )
    .otherwise(() => {
      // ...
    });

Infer the matching types from a pattern

P.infer<typeof pattern>

TS-Pattern is pretty handy for parsing unknown payloads like HTTP responses. You can write a pattern for the shape you are expecting, and then use isMatching(pattern, response) to make sure the response has the correct shape.

One limitation TS-Pattern had in its previous version was that it did not provide a way to get the TypeScript type of the value a given pattern matches. This is what P.infer<typeof pattern> does :)

const postPattern = {
  title: P.string,
  description: P.optional(P.string),
  content: P.string,
  likeCount: P.number,
};

type Post = P.infer<typeof postPattern>;
// Post: { title: string, description?: string, content: string, likeCount: number }

const userPattern = {
  name: P.string,
  postCount: P.number,
  bio: P.optional(P.string),
  posts: P.optional(P.array(postPattern)),
};

type User = P.infer<typeof userPattern>;
// User: { name: string, postCount: number, bio?: string, posts?: Post[]  }

const isUserList = isMatching(P.array(userPattern));

const res = await fetchUsers();

if (isUserList(res)) {
  // res: User
}

New type specific wildcards

P.symbol

P.symbol is a wildcard pattern matching any symbol.

match(Symbol('Hello'))
  .with(P.symbol, () => 'this is a symbol!')
  .exhaustive();

P.bigint

P.bigint is a wildcard pattern matching any bigint.

match(200n)
  .with(P.bigint, () => 'this is a bigint!')
  .exhaustive();