Skip to Content
Patterns GuideP Union And P Intersection

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

FeatureP.unionP.intersection
LogicOR (any match)AND (all match)
Type resultUnion typeIntersection type
Use caseAlternative valuesCombined 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) { // ... } })