Last Updated: 3/18/2026
P.union() and P.intersection()
These patterns let you combine multiple patterns into a single one, using either OR logic (P.union) or AND logic (P.intersection).
P.union(…patterns)
P.union matches if any of the provided patterns match. It’s the pattern equivalent of the || operator.
Basic Usage
import { match, P } from 'ts-pattern';
type Input =
| { type: 'user'; name: string }
| { type: 'org'; name: string }
| { type: 'text'; content: string };
const output = match(input)
.with(
{ type: P.union('user', 'org') },
(value) => {
// value: { type: 'user'; name: string } | { type: 'org'; name: string }
return value.name;
}
)
.with({ type: 'text' }, (value) => value.content)
.exhaustive();Multiple Value Matching
const classify = (status: string) =>
match(status)
.with(
P.union('idle', 'pending', 'loading'),
() => 'In progress'
)
.with(
P.union('success', 'complete'),
() => 'Done'
)
.with(
P.union('error', 'failed'),
() => 'Failed'
)
.otherwise(() => 'Unknown');Nested P.union
type Input = {
type: 'post';
content:
| { type: 'text'; text: string }
| { type: 'image'; src: string }
| { type: 'video'; url: string }
| { type: 'audio'; file: string };
};
match(input)
.with(
{
type: 'post',
content: {
type: P.union('image', 'video'),
// Matches image.src OR video.url
},
},
(post) => {
// post.content: { type: 'image'; src: string } | { type: 'video'; url: string }
return 'Media content';
}
)
.otherwise(() => 'Other');P.union with Complex Patterns
match(input)
.with(
{
value: P.union(
P.string.startsWith('http'),
P.string.startsWith('https'),
P.string.startsWith('ftp')
),
},
(input) => 'URL detected'
)
.otherwise(() => 'Not a URL');P.intersection(…patterns)
P.intersection matches if all of the provided patterns match. It’s the pattern equivalent of the && operator.
Basic Usage
class A {
constructor(public foo: 'bar' | 'baz') {}
}
class B {
constructor(public str: string) {}
}
type Input = { prop: A | B };
match(input)
.with(
{ prop: P.intersection(P.instanceOf(A), { foo: 'bar' }) },
({ prop }) => {
// prop: A & { foo: 'bar' }
return `A with foo=${prop.foo}`;
}
)
.with(
{ prop: P.intersection(P.instanceOf(A), { foo: 'baz' }) },
({ prop }) => {
// prop: A & { foo: 'baz' }
return `A with foo=${prop.foo}`;
}
)
.with({ prop: P.instanceOf(B) }, ({ prop }) => `B: ${prop.str}`)
.exhaustive();Intersection with Type Guards
const hasLength = (x: any): x is { length: number } =>
x != null && typeof x.length === 'number';
const isNonEmpty = (x: { length: number }): x is { length: number } =>
x.length > 0;
match(value)
.with(
P.intersection(P.when(hasLength), P.when(isNonEmpty)),
(value) => {
// value has length property and length > 0
return `Non-empty with length ${value.length}`;
}
)
.otherwise(() => 'Empty or no length');Multiple Constraints
type User = {
age: number;
verified: boolean;
role: 'admin' | 'user';
};
match(user)
.with(
P.intersection(
{ age: P.number.gte(18) },
{ verified: true },
{ role: 'admin' }
),
(user) => {
// user is adult, verified, and admin
return 'Full admin access';
}
)
.otherwise(() => 'Limited access');Combining Union and Intersection
type Response =
| { status: 'success'; data: { type: 'user' | 'admin'; name: string } }
| { status: 'error'; code: number };
match(response)
.with(
P.intersection(
{ status: 'success' },
{ data: { type: P.union('user', 'admin') } }
),
(response) => {
// response.status === 'success' AND
// response.data.type is 'user' or 'admin'
return `Welcome, ${response.data.name}`;
}
)
.otherwise(() => 'Error');Real-World Examples
Permission System
type Permission = 'read' | 'write' | 'delete' | 'admin';
type User = {
role: string;
permissions: Permission[];
verified: boolean;
};
const checkAccess = (user: User, resource: string) =>
match(user)
.with(
P.intersection(
{ role: 'admin' },
{ verified: true }
),
() => 'full-access'
)
.with(
{
permissions: P.array(P.union('write', 'delete')),
},
() => 'write-access'
)
.with(
{ permissions: P.array('read') },
() => 'read-only'
)
.otherwise(() => 'no-access');Form Validation
type FormData = {
email: string;
age: number;
consent: boolean;
};
const validateForm = (data: FormData) =>
match(data)
.with(
P.intersection(
{ email: P.string.includes('@') },
{ age: P.number.gte(18) },
{ consent: true }
),
() => ({ valid: true })
)
.otherwise(() => ({ valid: false, error: 'Invalid form data' }));HTTP Response Handler
type Response = {
status: number;
body: any;
headers: Record<string, string>;
};
const handleResponse = (response: Response) =>
match(response)
.with(
P.intersection(
{ status: P.number.between(200, 299) },
{ headers: { 'content-type': P.string.includes('json') } }
),
(res) => JSON.parse(res.body)
)
.with(
{ status: P.union(401, 403) },
() => {
throw new Error('Unauthorized');
}
)
.otherwise(() => null);Type Safety
Both P.union and P.intersection preserve type information:
type Status = 'idle' | 'loading' | 'success' | 'error';
declare const status: Status;
match(status)
.with(P.union('idle', 'loading'), (s) => {
// ✅ s: 'idle' | 'loading'
console.log(s);
})
.exhaustive();Comparison
| Feature | P.union | P.intersection |
|---|---|---|
| Logic | OR (any match) | AND (all match) |
| Type result | Union type | Intersection type |
| Use case | Alternative values | Combined constraints |
| Example | 'admin' | 'user' | Admin & Verified |
Best Practices
Prefer P.union for Simple Cases
// ✅ Good: Clean and readable
.with({ type: P.union('a', 'b', 'c') }, ...)
// ❌ Avoid: Overly complex
.with({ type: 'a' }, ...)
.with({ type: 'b' }, ...)
.with({ type: 'c' }, ...)Use P.intersection for Refinement
// ✅ Good: Refining a class instance
.with(
P.intersection(P.instanceOf(User), { verified: true }),
...
)
// ❌ Less ideal: Manual checks
.with(P.instanceOf(User), (user) => {
if (user.verified) {
// ...
}
})