Last Updated: 3/6/2026
Type Inference
TS-Pattern leverages TypeScript’s advanced type system features to provide precise type inference throughout your pattern matching expressions.
Automatic Type Narrowing
When pattern-matching on union types, TS-Pattern automatically narrows the type in your handler functions:
import { match, P } from 'ts-pattern';
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: Text
return `<p>${text.data}</p>`;
})
.with({ type: 'img' }, (img) => {
// img: Img
return `<img src="${img.data.src}" alt="${img.data.alt}" />`;
})
.with({ type: 'video' }, (video) => {
// video: Video
return `<video src="${video.data.src}" />`;
})
.exhaustive();Multiple Pattern Narrowing
When using multiple patterns in a single .with(), the type is narrowed to the union of all matching patterns:
match(content)
.with({ type: 'img' }, { type: 'video' }, (media) => {
// media: Img | Video
return `<source src="${media.data.src}" />`;
})
.with({ type: P.union('img', 'video') }, (media) => {
// media: Img | Video (same as above)
return `<source src="${media.data.src}" />`;
})
.otherwise(() => '');Selection Type Inference
When using P.select(), TS-Pattern infers the type of the selected value:
Anonymous Selection
match(content)
.with({ type: 'text', data: P.select() }, (data) => {
// data: string
return `<p>${data}</p>`;
})
.with({ type: 'video', data: { format: P.select() } }, (format) => {
// format: 'mp4' | 'webm'
return `Format: ${format}`;
})
.otherwise(() => '');Named Selections
match(content)
.with(
{
type: P.union('img', 'video'),
data: { src: P.select('src') },
},
({ src }) => {
// src: string
return `<source src="${src}" />`;
}
)
.otherwise(() => '');Selection with Sub-Patterns
type User = { age: number; name: string };
match(users)
.with(
P.array({ age: P.number.gte(18).select() }),
(ages) => {
// ages: number[]
return ages;
}
)
.otherwise(() => []);Type Guard Functions
When you pass a type guard function to P.when(), TS-Pattern uses its return type:
const isString = (x: unknown): x is string => typeof x === 'string';
const isNumber = (x: unknown): x is number => typeof x === 'number';
match(input)
.with({ id: P.when(isString) }, (obj) => {
// obj: { id: string }
return obj.id.toUpperCase();
})
.with({ id: P.when(isNumber) }, (obj) => {
// obj: { id: number }
return obj.id.toFixed(2);
})
.otherwise(() => '');Inferring Types from Patterns
Use P.infer to extract the TypeScript type that a pattern matches:
const userPattern = {
id: P.number,
name: P.string,
email: P.string,
age: P.optional(P.number),
posts: P.optional(
P.array({
title: P.string,
content: P.string,
})
),
} as const;
type User = P.infer<typeof userPattern>;
// User: {
// id: number;
// name: string;
// email: string;
// age?: number;
// posts?: { title: string; content: string }[];
// }
// Use with isMatching for runtime validation
const isUser = isMatching(userPattern);
if (isUser(data)) {
// data: User
console.log(data.name);
}Unknown Input Types
When your input is unknown or any, TS-Pattern infers the type from your pattern:
const input: unknown = { type: 'user', name: 'Alice', age: 30 };
match(input)
.with(
{ type: 'user', name: P.string, age: P.number },
(user) => {
// user: { type: 'user', name: string, age: number }
return `User: ${user.name}, Age: ${user.age}`;
}
)
.otherwise(() => 'Unknown');Exhaustiveness Checking
TS-Pattern tracks which cases you’ve handled and ensures all possibilities are covered:
type Status = 'idle' | 'loading' | 'success' | 'error';
const getStatusMessage = (status: Status) =>
match(status)
.with('idle', () => 'Ready')
.with('loading', () => 'Loading...')
.with('success', () => 'Done!')
// TypeScript error if 'error' case is missing
.exhaustive();Complex Union Exhaustiveness
type Permission = 'editor' | 'viewer';
type Plan = 'basic' | 'pro';
const fn = (org: Plan, user: Permission) =>
match([org, user])
.with(['basic', 'viewer'], () => 'basic viewer')
.with(['basic', 'editor'], () => 'basic editor')
.with(['pro', 'viewer'], () => 'pro viewer')
.with(['pro', 'editor'], () => 'pro editor')
.exhaustive(); // All 4 combinations handled ✓Narrowing with .narrow()
Use .narrow() to progressively refine types:
type Input = { color: 'red' | 'blue'; size: 'small' | 'large' };
match(input)
.with({ color: 'red', size: 'small' }, () => 'red small')
.narrow()
.otherwise((narrowed) => {
// narrowed: { color: 'red', size: 'large' }
// | { color: 'blue', size: 'small' }
// | { color: 'blue', size: 'large' }
});Best Practices
-
Use
as constwith patterns: For the most precise type inferenceconst pattern = { type: 'user', name: P.string } as const; -
Leverage exhaustiveness checking: Use
.exhaustive()with discriminated unions -
Type your input when possible: Better inference and error messages
// Good const fn = (status: Status) => match(status)... // Less ideal const fn = (status: any) => match(status)... -
Use type guards with P.when(): For custom type narrowing
-
Combine P.infer with isMatching: For runtime validation with type safety