Skip to Content
Migration GuidesV3 To V4 Migration Guide

Last Updated: 3/18/2026


TS-Pattern v3 to v4 Migration Guide

This guide covers all breaking changes and new features when migrating from version 3 to version 4 of TS-Pattern.

Breaking Changes

Import Changes

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

Before (v3)

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

After (v4)

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

Wildcard Pattern (__)

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

// v3 import { match, __ } from 'ts-pattern'; match(value) .with(__, (v) => `${v}`) .exhaustive(); // v4 import { match, P } from 'ts-pattern'; match(value) .with(P._, (v) => `${v}`) // OR .with(P.any, (v) => `${v}`) .exhaustive();

Pattern Functions: select(), not(), when()

Functions to create patterns have been moved to the P module:

// v3 import { match, select, not, when } from 'ts-pattern'; match(value) .with({ prop: select() }, ...) .with({ prop: not(10) }, ...) .with({ prop: when((x) => x < 5) }, ...) // v4 import { match, P } from 'ts-pattern'; match(value) .with({ prop: P.select() }, ...) .with({ prop: P.not(10) }, ...) .with({ prop: P.when((x) => x < 5) }, ...)

Pattern Type

The Pattern type is now accessible at P.Pattern:

// v3 import { Pattern } from 'ts-pattern'; const pattern: Pattern<number> = when(x => x > 2); // v4 import { P } from 'ts-pattern'; const pattern: P.Pattern<number> = P.when(x => x > 2);

Array Patterns

The syntax for matching arrays with unknown length has changed from [subpattern] to P.array(subpattern):

Before (v3)

import { match, __ } from 'ts-pattern'; const parseUsers = (response: unknown) => match(response) .with({ data: [{ name: __.string }] }, (users) => users) .otherwise(() => []);

After (v4)

import { match, P } from 'ts-pattern'; const parseUsers = (response: unknown) => match(response) .with({ data: P.array({ name: P.string }) }, (users) => users) .otherwise(() => []);

Important: Now [subpattern] matches arrays with exactly 1 element. This is more consistent with native destructuring.

NaN Pattern

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

// v3 match<number>(NaN) .with(__.NaN, () => "not a number") .otherwise((n) => n); // v4 match<number>(NaN) .with(NaN, () => "not a number") .otherwise((n) => n);

New Features

P.array(pattern)

Match arrays of elements with a specific pattern:

import { match, P } from 'ts-pattern'; const responsePattern = { data: P.array({ id: P.string, posts: P.array({ title: P.string, content: P.string, }), }), }; fetchSomething().then((value: unknown) => match(value) .with(responsePattern, (value) => { // value: { data: { id: string, posts: { title: string, content: string }[] }[] } return value; }) .otherwise(() => { throw new Error('unexpected response'); }) );

P.optional(pattern)

Mark object properties as optional:

match(user) .with( { type: 'user', detail: { bio: P.optional(P.string), socialLinks: P.optional({ twitter: P.select(), }), }, }, (twitterLink) => { // twitterLink: string | undefined } ) .exhaustive();

P.union(…patterns)

Match if any of the provided patterns match:

type Input = | { type: 'a'; value: string } | { type: 'b'; value: number } | { type: 'c'; value: { type: 'd'; value: boolean } }; match(input) .with( { type: P.union('a', 'b') }, (x) => { // x: { type: 'a'; value: string } | { type: 'b'; value: number } return 'branch 1'; } ) .with( { type: 'c', value: { value: P.union(P.boolean, P.string) } }, (x) => 'branch 2' ) .exhaustive();

P.intersection(…patterns)

Match if all provided patterns match:

class A { constructor(public foo: 'bar' | 'baz') {} } class B { constructor(public str: string) {} } match(input) .with( { prop: P.intersection(P.instanceOf(A), { foo: 'bar' }) }, ({ prop }) => { // prop: A & { foo: 'bar' } return 'branch 1'; } ) .exhaustive();

P.select() with Sub-patterns

P.select() can now take a sub-pattern to only select matching values:

type User = { type: 'user'; username: string }; type Org = { type: 'org'; orgId: number }; match(input) .with( { author: P.select({ type: 'user' }) }, (user) => { // user: User } ) .with( { author: P.select('org', { type: 'org' }), content: P.select('text', { type: 'text' }), }, ({ org, text }) => { // org: Org, text: Text } ) .exhaustive();

P.infer<typeof pattern>

Infer TypeScript types from patterns:

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, posts: P.optional(P.array(postPattern)), }; type User = P.infer<typeof userPattern>; // User: { name: string, posts?: Post[] } const isUserList = isMatching(P.array(userPattern)); if (isUserList(response)) { // response: User[] }

New Wildcards

P.symbol

Matches any symbol:

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

P.bigint

Matches any bigint:

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

Migration Checklist

  • Replace all __ imports with P
  • Update __.string, __.number, etc. to P.string, P.number
  • Change __ wildcard to P._ or P.any
  • Move select(), not(), when() to P.select(), P.not(), P.when()
  • Update Pattern<T> type usage to P.Pattern<T>
  • Replace array patterns [pattern] with P.array(pattern)
  • Replace __.NaN with NaN
  • Test all pattern matching expressions