Skip to Content
Patterns GuideP Optional And P Instanceof

Last Updated: 3/18/2026


P.optional() and P.instanceOf()

These patterns handle optional properties and class instance matching.

P.optional(pattern)

P.optional() marks a property as optional in object patterns. The property can be undefined or match the provided sub-pattern.

Basic Usage

import { match, P } from 'ts-pattern'; type User = { name: string; email?: string; bio?: string; }; match(user) .with( { name: P.string, email: P.optional(P.string), }, (user) => { // user.name: string // user.email: string | undefined return user.email ? `Email: ${user.email}` : 'No email'; } ) .exhaustive();

Why P.optional() is Needed

Without P.optional(), patterns require the property to be present:

type Config = { host: string; port?: number; }; // ❌ This won't match if port is undefined match(config) .with({ host: P.string, port: P.number }, ...) // ✅ This matches whether port is defined or not match(config) .with({ host: P.string, port: P.optional(P.number) }, ...)

With P.select()

Selected values from optional patterns are typed as T | undefined:

type Post = { title: string; description?: string; author?: { name: string }; }; match(post) .with( { title: P.string, description: P.optional(P.string.select()), }, (description) => { // description: string | undefined return description || 'No description'; } ) .exhaustive();

Named Selections

match(post) .with( { title: P.select('title'), description: P.optional(P.string.select('desc')), author: P.optional(P.select('author')), }, ({ title, desc, author }) => { // title: string // desc: string | undefined // author: { name: string } | undefined return { title, description: desc || 'No description', authorName: author?.name || 'Anonymous', }; } ) .exhaustive();

Nested Optional Properties

type Article = { title: string; metadata?: { tags?: string[]; category?: string; }; }; match(article) .with( { metadata: P.optional({ tags: P.optional(P.array(P.string)), category: P.optional(P.string), }), }, (article) => { const tags = article.metadata?.tags || []; const category = article.metadata?.category || 'Uncategorized'; return { tags, category }; } ) .exhaustive();

Real-World Example: Form Validation

type FormData = { username: string; email: string; phone?: string; address?: { street?: string; city?: string; zipCode?: string; }; }; const validateForm = (data: FormData) => match(data) .with( { username: P.string.minLength(3), email: P.string.includes('@'), phone: P.optional(P.string.regex(/^\d{10}$/)), address: P.optional({ city: P.optional(P.string), zipCode: P.optional(P.string.regex(/^\d{5}$/)), }), }, () => ({ valid: true }) ) .otherwise(() => ({ valid: false, error: 'Invalid form data' }));

P.instanceOf(Class)

P.instanceOf() matches values that are instances of a specific class. It uses JavaScript’s instanceof operator under the hood.

Basic Usage

class User { constructor(public name: string) {} } class Admin extends User { constructor(name: string, public level: number) { super(name); } } match(value) .with(P.instanceOf(Admin), (admin) => { // admin: Admin return `Admin ${admin.name} (level ${admin.level})`; }) .with(P.instanceOf(User), (user) => { // user: User return `User ${user.name}`; }) .otherwise(() => 'Not a user');

Built-in Classes

Works with built-in JavaScript classes:

match(value) .with(P.instanceOf(Date), (date) => { // date: Date return date.toISOString(); }) .with(P.instanceOf(Error), (error) => { // error: Error return `Error: ${error.message}`; }) .with(P.instanceOf(RegExp), (regex) => { // regex: RegExp return `Pattern: ${regex.source}`; }) .otherwise(() => 'Unknown type');

Error Handling

type Result<T> = { success: true; data: T } | { success: false; error: Error }; const handleResult = <T>(result: Result<T>) => match(result) .with({ success: true }, ({ data }) => data) .with( { success: false, error: P.instanceOf(TypeError) }, ({ error }) => { console.error('Type error:', error.message); return null; } ) .with( { success: false, error: P.instanceOf(Error) }, ({ error }) => { console.error('General error:', error.message); return null; } ) .exhaustive();

With P.intersection()

Combine P.instanceOf() with other patterns using P.intersection():

class Vehicle { constructor(public brand: string) {} } class Car extends Vehicle { constructor(brand: string, public doors: number) { super(brand); } } match(value) .with( P.intersection(P.instanceOf(Car), { doors: 4 }), (car) => { // car: Car & { doors: 4 } return `4-door ${car.brand}`; } ) .with( P.intersection(P.instanceOf(Car), { doors: 2 }), (car) => { // car: Car & { doors: 2 } return `2-door ${car.brand}`; } ) .otherwise(() => 'Not a car');

Custom Class Hierarchies

abstract class Shape { abstract area(): number; } class Circle extends Shape { constructor(public radius: number) { super(); } area() { return Math.PI * this.radius ** 2; } } class Rectangle extends Shape { constructor(public width: number, public height: number) { super(); } area() { return this.width * this.height; } } const getShapeInfo = (shape: Shape) => match(shape) .with(P.instanceOf(Circle), (circle) => ({ type: 'circle', area: circle.area(), radius: circle.radius, })) .with(P.instanceOf(Rectangle), (rect) => ({ type: 'rectangle', area: rect.area(), dimensions: `${rect.width}x${rect.height}`, })) .exhaustive();

API Response Parsing

class ApiError extends Error { constructor(public statusCode: number, message: string) { super(message); this.name = 'ApiError'; } } class NetworkError extends Error { constructor(message: string) { super(message); this.name = 'NetworkError'; } } const handleApiCall = async () => { try { return await fetchData(); } catch (error) { return match(error) .with(P.instanceOf(ApiError), (err) => ({ type: 'api-error', status: err.statusCode, message: err.message, })) .with(P.instanceOf(NetworkError), (err) => ({ type: 'network-error', message: 'Connection failed', })) .with(P.instanceOf(Error), (err) => ({ type: 'unknown-error', message: err.message, })) .otherwise(() => ({ type: 'unknown', message: 'An unknown error occurred', })); } };

Combining P.optional() and P.instanceOf()

type Config = { logger?: Console; errorHandler?: Error; timeout?: number; }; match(config) .with( { logger: P.optional(P.instanceOf(Console)), errorHandler: P.optional(P.instanceOf(Error)), timeout: P.optional(P.number.positive()), }, (cfg) => 'Valid config' ) .otherwise(() => 'Invalid config');

Best Practices

P.optional() vs undefined

// ✅ Good: Use P.optional() for optional properties .with({ name: P.string, email: P.optional(P.string) }, ...) // ❌ Don't: Explicitly matching undefined is less clean .with({ name: P.string, email: P.union(P.string, undefined) }, ...)

P.instanceOf() with Type Guards

// ✅ Good: Let P.instanceOf() handle type narrowing .with(P.instanceOf(Error), (err) => { // err is automatically typed as Error console.log(err.message); }) // ❌ Don't: Manual instanceof checks .when((x) => x instanceof Error, (x) => { // x might not be properly typed })

Combining for Complex Validation

class CustomError extends Error { constructor(public code: number, message: string) { super(message); } } type Response = { data?: any; error?: Error; metadata?: { timestamp: Date }; }; match(response) .with( { data: P.optional(P._), error: P.optional(P.instanceOf(CustomError)), metadata: P.optional({ timestamp: P.instanceOf(Date) }), }, (res) => 'Valid response structure' ) .otherwise(() => 'Invalid response');