Last Updated: 3/6/2026
P.when() - Custom Predicates
P.when() allows you to define custom logic to determine if a pattern should match. This is useful when the built-in patterns aren’t expressive enough for your use case.
Basic Usage
import { match, P } from 'ts-pattern';
type Input = { score: number };
const getGrade = (input: Input) =>
match(input)
.with(
{ score: P.when((score) => score >= 90) },
() => 'A'
)
.with(
{ score: P.when((score) => score >= 80) },
() => 'B'
)
.with(
{ score: P.when((score) => score >= 70) },
() => 'C'
)
.otherwise(() => 'F');
console.log(getGrade({ score: 95 })); // 'A'Type Guard Functions
You can use TypeScript type guard functions with P.when() for type narrowing:
const isString = (x: unknown): x is string => typeof x === 'string';
const isNumber = (x: unknown): x is number => typeof x === 'number';
const fn = (input: { id: number | string }) =>
match(input)
.with(
{ id: P.when(isString) },
(narrowed) => {
// narrowed: { id: string }
return narrowed.id.toUpperCase();
}
)
.with(
{ id: P.when(isNumber) },
(narrowed) => {
// narrowed: { id: number }
return narrowed.id.toFixed(2);
}
)
.exhaustive();Complex Conditions
Date Comparisons
type Event = {
name: string;
timestamp: number;
};
const now = Date.now();
const oneHourAgo = now - 3600000;
match(event)
.with(
{ timestamp: P.when((t) => t > oneHourAgo) },
(e) => `Recent: ${e.name}`
)
.otherwise((e) => `Old: ${e.name}`);String Validation
const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
match(user)
.with(
{ email: P.when(isValidEmail) },
(u) => `Valid email: ${u.email}`
)
.otherwise(() => 'Invalid email');Range Checks
const inRange = (min: number, max: number) => (value: number) =>
value >= min && value <= max;
match(temperature)
.with(
{ celsius: P.when(inRange(20, 25)) },
() => 'Comfortable'
)
.with(
{ celsius: P.when((c) => c < 20) },
() => 'Cold'
)
.with(
{ celsius: P.when((c) => c > 25) },
() => 'Hot'
)
.exhaustive();Combining with Other Patterns
With P.select()
match(data)
.with(
{
value: P.when((v) => v > 100).select(),
},
(value) => {
// value: number (and > 100)
return `Large value: ${value}`;
}
)
.otherwise(() => 'Small value');With P.array()
match(data)
.with(
{
items: P.array(P.when((item) => item.priority === 'high')),
},
(data) => 'All high priority items'
)
.otherwise(() => 'Mixed priorities');Nested Predicates
type User = {
name: string;
age: number;
permissions: string[];
};
match(user)
.with(
{
age: P.when((age) => age >= 18),
permissions: P.when((perms) => perms.includes('admin')),
},
(u) => `Adult admin: ${u.name}`
)
.otherwise(() => 'Regular user');Difference from .when()
There are two ways to use predicates in TS-Pattern:
P.when() - Pattern Predicate
Used inside a pattern:
match(value)
.with(
{ score: P.when((s) => s > 90) },
(v) => 'high score'
)
.otherwise(() => 'low score');.when() - Match Clause Predicate
Used as a top-level match clause:
match(value)
.when(
(v) => v.score > 90,
(v) => 'high score'
)
.otherwise(() => 'low score');Key differences:
P.when()can be nested in patterns and combined with other patterns.when()checks the entire input value- Use
P.when()for property-level checks,.when()for object-level checks
Performance Considerations
Predicate functions are executed at runtime, so:
- Keep predicates simple: Complex logic can slow down matching
- Avoid side effects: Predicates should be pure functions
- Consider caching: For expensive computations, cache results
// Good: Simple and fast
P.when((x) => x > 10)
// Less ideal: Complex computation
P.when((x) => expensiveCalculation(x) > threshold)
// Better: Cache the result
const threshold = expensiveCalculation(input);
P.when((x) => x > threshold)Real-World Examples
Form Validation
type FormData = {
username: string;
password: string;
age: number;
};
const validateForm = (data: FormData) =>
match(data)
.with(
{
username: P.when((u) => u.length >= 3 && u.length <= 20),
password: P.when((p) => p.length >= 8),
age: P.when((a) => a >= 13),
},
() => ({ valid: true })
)
.otherwise(() => ({ valid: false, error: 'Invalid input' }));Business Logic
type Order = {
total: number;
isPremium: boolean;
items: number;
};
const calculateShipping = (order: Order) =>
match(order)
.with(
{ total: P.when((t) => t > 100) },
() => 0 // Free shipping
)
.with(
{
isPremium: true,
items: P.when((i) => i > 0),
},
() => 0 // Free for premium
)
.with(
{ items: P.when((i) => i <= 3) },
() => 5
)
.otherwise(() => 10);Best Practices
- Use type guards for type narrowing: Helps TypeScript understand your intent
- Keep predicates pure: No side effects or mutations
- Name your predicate functions: Makes code more readable
- Combine with built-in patterns: Use
P.string,P.numberbeforeP.when()
// Good: Named, pure function
const isAdult = (age: number) => age >= 18;
match(user)
.with({ age: P.when(isAdult) }, handleAdult)
.otherwise(handleMinor);
// Less ideal: Inline complex logic
match(user)
.with(
{ age: P.when((a) => a >= 18 && a < 65 && !isRetired(a)) },
handler
);