Skip to Content
Type Inference

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

  1. Use as const with patterns: For the most precise type inference

    const pattern = { type: 'user', name: P.string } as const;
  2. Leverage exhaustiveness checking: Use .exhaustive() with discriminated unions

  3. 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)...
  4. Use type guards with P.when(): For custom type narrowing

  5. Combine P.infer with isMatching: For runtime validation with type safety