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');