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 withP - Update
__.string,__.number, etc. toP.string,P.number - Change
__wildcard toP._orP.any - Move
select(),not(),when()toP.select(),P.not(),P.when() - Update
Pattern<T>type usage toP.Pattern<T> - Replace array patterns
[pattern]withP.array(pattern) - Replace
__.NaNwithNaN - Test all pattern matching expressions