Last Updated: 3/18/2026
Additional Methods
Beyond the core .with(), .exhaustive(), and .otherwise() methods, TS-Pattern provides additional methods for specific use cases.
.when()
The .when() method adds a case that matches based on a predicate function, without requiring a pattern.
Signature
function when(
predicate: (value: TInput) => unknown,
handler: (value: TInput) => TOutput
): Match<TInput, TOutput>;Basic Usage
import { match } from 'ts-pattern';
type Input = { score: number };
const output = match<Input>({ score: 85 })
.when(
(input) => input.score >= 90,
() => 'A - Excellent'
)
.when(
(input) => input.score >= 80,
() => 'B - Good'
)
.when(
(input) => input.score >= 70,
() => 'C - Average'
)
.otherwise(() => 'F - Fail');
console.log(output); // => 'B - Good'When vs. P.when
.when(): Top-level method, matches entire inputP.when(): Pattern function, can be nested in object/array patterns
// Using .when() - matches entire input
match(input)
.when((x) => x > 10, () => 'Greater than 10')
.exhaustive();
// Using P.when() - matches nested property
match(input)
.with({ value: P.when((x) => x > 10) }, () => 'Value > 10')
.exhaustive();Combining .when() and .with()
match(data)
.with({ type: 'user' }, () => 'User type')
.when((d) => d.priority > 5, () => 'High priority')
.when((d) => d.items.length === 0, () => 'Empty items')
.otherwise(() => 'Default');.returnType<T>()
Explicitly specify the return type of your match expression without setting the input type.
Signature
function returnType<TOutputOverride>(): Match<TInput, TOutputOverride>;Basic Usage
match({ isAdmin, plan })
.returnType<number>() // All branches must return number
.with({ isAdmin: true }, () => 100)
.with({ plan: 'premium' }, () => 50)
.with({ plan: 'free' }, () => 'Oops!') // ❌ Type error: not a number
.exhaustive();When to Use
Before .returnType():
// Had to specify both input and output types
match<
{ isAdmin: boolean; plan: 'free' | 'premium' }, // ⚠️ Input type
number // Output type
>({ isAdmin, plan })
.with({ isAdmin: true }, () => 100)
.exhaustive();With .returnType():
// Only specify output type, input is inferred
match({ isAdmin, plan })
.returnType<number>() // ✅ Just the output type
.with({ isAdmin: true }, () => 100)
.exhaustive();Complex Return Types
type Result =
| { success: true; data: string }
| { success: false; error: Error };
const process = (input: unknown) =>
match(input)
.returnType<Result>()
.with(P.string, (str) => ({ success: true, data: str }))
.with(P.number, () => ({ success: true, data: 'number' }))
.otherwise(() => ({ success: false, error: new Error('Invalid') }));.narrow()
The .narrow() method deeply narrows the input type to exclude all values that have been previously handled.
Signature
function narrow(): Match<Narrowed<TInput>, TOutput>;Basic Usage
type Input = { color: 'red' | 'blue'; size: 'small' | 'large' };
declare const input: Input;
const result = match(input)
.with({ color: 'red', size: 'small' }, () => 'Red small')
.with({ color: 'blue', size: 'large' }, () => 'Blue large')
.narrow() // 👈 Narrows remaining cases
.otherwise((narrowedInput) => {
// narrowedInput type:
// | { color: 'red'; size: 'large' }
// | { color: 'blue'; size: 'small' }
return 'Other combination';
});Narrowing Union Types
type Status = 'idle' | 'loading' | 'success' | 'error';
declare const status: Status;
match(status)
.with('idle', () => 'Not started')
.with('loading', () => 'In progress')
.narrow()
.otherwise((remaining) => {
// remaining: 'success' | 'error'
return `Final state: ${remaining}`;
});Nested Property Narrowing
The real power of .narrow() is narrowing nested properties:
type Message =
| { type: 'text'; content: string; priority: 'high' | 'low' }
| { type: 'image'; url: string };
declare const message: Message;
match(message)
.with({ type: 'text', priority: 'high' }, () => 'High priority text')
.narrow()
.with({ type: 'text' }, (msg) => {
// msg: { type: 'text'; content: string; priority: 'low' }
// priority is narrowed to 'low'
return `Low priority: ${msg.content}`;
})
.with({ type: 'image' }, (msg) => `Image: ${msg.url}`)
.exhaustive();When to Use .narrow()
Without .narrow():
// TypeScript doesn't automatically narrow nested properties
match(input)
.with({ color: 'red', size: 'small' }, ...)
.otherwise((input) => {
// input still has full type:
// { color: 'red' | 'blue'; size: 'small' | 'large' }
// ⚠️ Not narrowed!
});With .narrow():
match(input)
.with({ color: 'red', size: 'small' }, ...)
.narrow() // 👈
.otherwise((input) => {
// input is narrowed:
// | { color: 'red'; size: 'large' }
// | { color: 'blue'; size: 'small' }
// | { color: 'blue'; size: 'large' }
// ✅ Previous case excluded!
});.run()
Executes the pattern matching expression and returns the result, without exhaustiveness checking.
Signature
function run(): TOutput;Basic Usage
const output = match(input)
.with(pattern1, handler1)
.with(pattern2, handler2)
.run(); // ⚠️ May throw if no pattern matchesComparison with .exhaustive()
| Feature | .exhaustive() | .run() |
|---|---|---|
| Compile-time checking | ✅ Yes | ❌ No |
| Throws if unmatched | ✅ Yes | ✅ Yes |
| Type safety | High | Lower |
| Use case | Production code | Quick prototypes |
When to Use .run()
Use .exhaustive() (preferred):
// ✅ Safe: TypeScript ensures all cases are covered
type Status = 'idle' | 'loading' | 'success';
match(status)
.with('idle', () => 'Not started')
.with('loading', () => 'In progress')
.with('success', () => 'Done')
.exhaustive(); // TypeScript checks all Status values are handledUse .run() (acceptable for certain cases):
// When input type is very broad (unknown, any)
const processUnknown = (input: unknown) =>
match(input)
.with(P.string, (s) => s.toUpperCase())
.with(P.number, (n) => n * 2)
.run(); // Exhaustiveness checking isn't practical hereError Handling
Both .run() and .exhaustive() throw if no pattern matches:
try {
match(input)
.with({ type: 'a' }, () => 'A')
.run(); // Throws if input.type !== 'a'
} catch (error) {
console.error('No pattern matched');
}Combining Methods
These methods can be combined for powerful pattern matching:
type Data = {
status: 'pending' | 'approved' | 'rejected';
priority: 'low' | 'medium' | 'high';
value: number;
};
const process = (data: Data) =>
match(data)
.returnType<string>() // Specify return type
.with({ status: 'approved', priority: 'high' }, () => 'Fast track')
.narrow() // Narrow remaining cases
.when(
(d) => d.value > 1000, // Custom predicate
() => 'High value item'
)
.with({ status: 'rejected' }, () => 'Rejected')
.exhaustive(); // Ensure all cases coveredBest Practices
Prefer .exhaustive() over .run()
// ✅ Good: Exhaustiveness checking
match(status)
.with('idle', ...)
.with('loading', ...)
.exhaustive();
// ❌ Avoid in production
match(status)
.with('idle', ...)
.run(); // May throw at runtimeUse .returnType() for Clear Contracts
// ✅ Good: Return type is explicit
function getUserRole(user: User): 'admin' | 'user' | 'guest' {
return match(user)
.returnType<'admin' | 'user' | 'guest'>()
.with({ role: 'admin' }, () => 'admin')
.with({ role: 'user' }, () => 'user')
.otherwise(() => 'guest');
}Use .narrow() for Complex Unions
// ✅ Good: Narrow for precise types in .otherwise()
match(complexUnion)
.with(specificCase1, ...)
.with(specificCase2, ...)
.narrow()
.otherwise((remaining) => {
// remaining is precisely typed
});