Skip to Content
When

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:

  1. Keep predicates simple: Complex logic can slow down matching
  2. Avoid side effects: Predicates should be pure functions
  3. 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

  1. Use type guards for type narrowing: Helps TypeScript understand your intent
  2. Keep predicates pure: No side effects or mutations
  3. Name your predicate functions: Makes code more readable
  4. Combine with built-in patterns: Use P.string, P.number before P.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 );