| name | ts-match |
| description | This skill should be used when an agent is writing or reviewing TypeScript code that uses ts-match (published as @diegogbrisa/ts-match) for pattern matching, exhaustive discriminated-union handling, promise-aware branching, runtime validation, grouped cases, pattern helpers, or boundary assertions. |
ts-match usage skill
What the library is for
ts-match is a TypeScript-first pattern matching library with strong handler inference, exhaustive handling of closed unions, promise-aware terminals, runtime validation helpers, grouped discriminant cases, and zero runtime dependencies. Install and import it as @diegogbrisa/ts-match.
Use ts-match when code benefits from:
- exhaustive closed-union handling;
- narrowed handler parameters without casts;
- structural object/tuple/array/record/map/set matching;
- reusable runtime validators at boundaries;
- promise-backed inputs with one normalized terminal promise;
- discriminant/path dispatch through
matchBy.
Keep simple if conditions when a normal condition is clearer.
Primary public APIs
match(value) — synchronous structural/value pattern matching.
match.promise(valueOrPromise) — promise-aware structural/value matching. Resolves values, promises, thenables, and PromiseLike inputs internally; handlers receive Awaited<TInput>.
matchBy(value, path) — synchronous discriminant/path matching. Handlers receive the full narrowed input value.
matchBy.promise(valueOrPromise, path) — promise-aware discriminant/path matching. Paths, tags, maps, groups, and handlers infer from Awaited<TInput>.
P — namespace of reusable pattern helpers.
- Named
p* helpers — focused named exports equivalent to P.* helpers.
group(...) — reusable grouped matchBy case entry helper.
isMatching(pattern, value) / isMatching(pattern)(value) — runtime type guards.
assertMatching(pattern, value) — boundary assertion that throws PatternMismatchError on mismatch.
NonExhaustiveMatchError, PatternMismatchError — public error classes.
MatchPromiseResult<T> — safe promise terminal result: { ok: true; value: T } | { ok: false; error: unknown }.
Hard rules for agents
- Import only public APIs from
@diegogbrisa/ts-match or documented package subpaths.
- Never import from
src, dist, or internal files in user code.
- Do not invent helpers. Use only APIs listed here or in the README.
- Prefer chained
.with(...).exhaustive() for closed unions in normal application code.
- Use variadic
.with(pattern1, pattern2, handler) or .with(tag1, tag2, handler) as the default way to share one handler across a small number of equivalent patterns/tags.
- Do not convert an otherwise simple
.with(...) chain to .cases((group) => [...]) just because one branch has shared tags.
- Use
.otherwise(...) only when fallback behavior is intentional.
- Use
match.promise(...) or matchBy.promise(...) primarily when the matched input itself is promise-backed or may be promise-backed. Promise builders resolve the input internally; handlers receive Awaited<TInput>.
- Do not choose promise builders merely because branch handlers are async. If the input is already resolved, use plain
match(...) or matchBy(...); awaiting the terminal result is fine when handlers return promises.
- Do not use unsafe TypeScript casts. Do not add const assertions to inline ts-match arrays just to make inference work.
- Do not use broad
any in examples or generated code.
- Keep examples real and inference-first: no fake
const input: unknown = ..., no direct JSON.parse(...) typed as trusted data, no assertion-style endings, and no return/result annotations unless they are genuinely needed.
- Do not add wrapper functions just to prove an API; use a small app case where the function would naturally exist.
- Do not use
switch in generated examples unless explicitly writing a short before/after comparison requested by the user.
- Avoid inline object-map
.cases({...}) in hot loops. Prefer .with(...).exhaustive() unless the user explicitly accepts the manual-typing tradeoff of hoisted case maps.
- When TypeScript reports a
ts-match: diagnostic, read that payload first and fix the modeled issue. Do not silence it with casts, any, or a rewrite to switch.
Valid imports
Root import for normal usage:
import { assertMatching, group, isMatching, match, matchBy, P } from '@diegogbrisa/ts-match'
import type { MatchByPath, MatchedValue, MatchPromiseResult, TemporalInstantValue } from '@diegogbrisa/ts-match'
Focused subpaths:
import { match } from '@diegogbrisa/ts-match/match'
import { matchBy } from '@diegogbrisa/ts-match/match-by'
import { P, pCollect, pRegex, pString } from '@diegogbrisa/ts-match/patterns'
import { isMatching, assertMatching } from '@diegogbrisa/ts-match/assertions'
import { NonExhaustiveMatchError, PatternMismatchError } from '@diegogbrisa/ts-match/errors'
import { group } from '@diegogbrisa/ts-match/group'
There is no default export.
The package is ESM-first. CommonJS require(...) and no-type tsx scripts are supported through the package export map for scripts and tooling, but normal TypeScript examples should prefer ESM imports.
Focused subpath type exports
Use these only when accepting/forwarding builders or writing library integrations. Most application code should rely on inference.
@diegogbrisa/ts-match/match: match, SyncMatchBuilder, PromiseMatchBuilder, MatchFunction, MatchedValue, MatchPromiseResult.
@diegogbrisa/ts-match/match-by: matchBy, SyncMatchByBuilder, PromiseMatchByBuilder, MatchByBuilder, MatchByFunction, MatchByPath, MatchPromiseResult.
@diegogbrisa/ts-match/patterns: P and every public p* helper.
@diegogbrisa/ts-match/assertions: isMatching, assertMatching.
@diegogbrisa/ts-match/errors: NonExhaustiveMatchError, PatternMismatchError, preview, MatchErrorMetadata.
@diegogbrisa/ts-match/group: group.
Choosing the right matcher
Use matchBy when one key/path decides a discriminated union branch:
const next = matchBy(action, 'type')
.with('start', (action) => startState(action.id))
.with('success', (action) => readyState(action.rows))
.with('failure', (action) => failedState(action.message))
.exhaustive()
Use match when matching structure, tuples, arrays, predicates, selections, records, exact objects, or non-discriminant values:
const displayName = match(profile)
.with({ type: 'user', profile: { name: P.select('name', P.string) } }, ({ name }) => name)
.with({ type: 'team', name: P.select('name', P.string) }, ({ name }) => name)
.otherwise(() => 'Guest')
Use promise builders when the input itself may be async. Pass the promise-producing expression directly so the builder resolves it and matches the resolved value:
const status = await matchBy
.promise(fetchOrder('order-1'), 'state')
.with('paid', (order) => ({ type: 'readyToShip', orderId: order.id }))
.otherwise(() => ({ type: 'needsReview' }))
Use plain match(...) / matchBy(...) when the input is already resolved, even if one or more handlers return promises. Use sync match(promise) only if you intentionally want to match the Promise object itself.
match(value) use cases
Literal and structural branches
const label = match(status)
.with('ready', () => 'Ready')
.with(0, () => 'No items')
.with({ ok: true }, (value) => value.body)
.otherwise(() => 'Needs attention')
Plain literals, object patterns, bare tuple arrays, and every P.* helper are valid patterns. Literal equality uses Object.is.
Multiple patterns sharing one handler
const status = match(state)
.with('idle', 'loading', () => 'pending')
.with('success', () => 'done')
.exhaustive()
.when(predicate, handler)
Use .when(...) for value-level predicates that are easier to express as functions:
function discountLabel(percent: number) {
return match(percent)
.when(
(value) => value > 0,
(value) => `${String(value)}% off`,
)
.otherwise(() => 'No discount')
}
Use P.when(predicate) when the predicate should be nested inside another pattern.
.otherwise(handler)
Use .otherwise(...) for open inputs or intentional fallback behavior. The fallback receives the remaining unmatched value type.
const size = match(value)
.with(P.string, (value) => value.length)
.otherwise(() => 0)
.exhaustive()
Use .exhaustive() for closed unions. TypeScript rejects it while known cases remain unhandled.
const text = match(result)
.with({ type: 'success' }, (value) => value.data)
.with({ type: 'error' }, (value) => value.message)
.with({ type: 'idle' }, () => 'idle')
.exhaustive()
At runtime, unexpected unhandled data throws NonExhaustiveMatchError.
Selections
No P.select: handler receives the matched value.
match(user).with({ type: 'user' }, (user) => user.id)
One anonymous P.select(): handler receives the selected value.
match(user).with({ profile: { name: P.select() } }, (name) => name)
Named selections: handler receives an object of selected values.
match(user).with(
{ name: P.select('name', P.string), age: P.select('age', P.number) },
({ name, age }) => `${name}:${age}`,
)
Do not mix anonymous and named selections in one successful pattern.
Collection captures
Use P.collect(name, pattern) inside repeated containers when the handler should receive many matched values as a named array. It is valid inside P.array(...), P.nonEmptyArray(...), P.record(...), P.nonEmptyRecord(...), P.map(...), and P.set(...).
match(['u1', 42, 'u2']).with(
P.array(P.union(P.collect('ids', P.string), P.collect('counts', P.number))),
({ ids, counts }) => ({ ids, counts }),
)
P.collect(...) is not a filter. The inner pattern must match; use P.union(...) when repeated values can have several allowed shapes. Empty repeated containers that match provide empty arrays for known collection names. Duplicate collection names append and union their element types. Collection captures can mix with named P.select(...), but not anonymous P.select(), and collection names cannot collide with named selection names.
Rendering UI from typed data
Use JSX examples when they make the pattern more visual: a match expression returns the selected handler's value, so branches can return components. Keep the scenario generic and self-contained.
import { match, P } from '@diegogbrisa/ts-match'
type ProductContent = { type: 'text'; body: string } | { type: 'image'; src: string; alt: string }
type ProductResult =
| { status: 'loading' }
| { status: 'success'; product: { title: string; content: ProductContent } }
| { status: 'error'; error: Error }
function ProductPreview({ result }: { result: ProductResult }) {
return match(result)
.with({ status: 'loading' }, () => <p>Loading product…</p>)
.with({ status: 'error' }, ({ error }) => <p role="alert">{error.message}</p>)
.with({ status: 'success', product: { content: { type: 'text' } } }, ({ product }) => (
<article>
<h2>{product.title}</h2>
<p>{product.content.body}</p>
</article>
))
.with(
{ status: 'success', product: { content: { type: 'image', src: P.select('src'), alt: P.select('alt') } } },
({ src, alt }) => <img src={src} alt={alt} />,
)
.exhaustive()
}
React is only an example consumer; do not imply ts-match depends on React. Prefer generic UI states such as products, checkout, onboarding, routes, forms, or API results over one app's private domain.
match.promise(valueOrPromise) use cases
Promise builders are for promise-backed inputs. They accept T | PromiseLike<T>, including thenables, resolve the input internally, and pass Awaited<TInput> to handlers. Prefer passing the promise-producing expression directly instead of awaiting into a temporary value and then matching.
Do not use match.promise(...) solely because branch handlers return promises. If the matched value is already resolved, use match(value) and await the terminal result when needed.
type ProfileResponse =
| { ok: true; profile: { id: string; name: string } }
| { ok: false; status: number; message: string }
const profileResponse: ProfileResponse = { ok: true, profile: { id: 'user-1', name: 'Ada' } }
const missingProfile: ProfileResponse = { ok: false, status: 404, message: 'missing' }
const responses: readonly ProfileResponse[] = [profileResponse, missingProfile]
const profilePromise = Promise.resolve(responses[0] ?? missingProfile)
const name = await match
.promise(profilePromise)
.with({ ok: true, profile: { name: P.select('name', P.string) } }, ({ name }) => name)
.with({ ok: false }, ({ message }) => message)
.exhaustive()
Normal terminals reject for input rejection, pattern/predicate errors, handler throws/rejections, fallback throws/rejections, and defensive non-exhaustiveness. .otherwise(...) is only a pattern fallback; it does not catch input rejection.
Promise normal terminals
const result = match
.promise(profilePromise)
.with({ ok: true }, (value) => value.profile.name)
.with({ ok: false }, async (value) => value.message)
.exhaustive()
Handler return values are awaited and unwrapped, so promise-returning and plain branches produce one terminal promise. This is supported behavior, but it is not by itself a reason to choose match.promise(...) over match(...) when the matched input is already resolved.
Promise safe terminals
Safe terminals exist only on promise builders.
const missingProfile: ProfileResponse = { ok: false, status: 404, message: 'missing' }
const missingResponses: readonly ProfileResponse[] = [missingProfile]
const missingProfilePromise = Promise.resolve(missingResponses[0] ?? missingProfile)
const result = await match
.promise(missingProfilePromise)
.with({ ok: true, profile: { name: P.select('name', P.string) } }, ({ name }) => name)
.safeOtherwise(() => 'Guest')
if (result.ok) {
result.value
} else {
result.error
}
safeExhaustive() preserves compile-time exhaustiveness exactly like .exhaustive().
safeOtherwise(handler) requires a fallback handler; there is no no-argument form.
- Safe results have type
Promise<MatchPromiseResult<Output>>.
- Safe success values are awaited before wrapping.
- Safe errors are
unknown and are the original thrown/rejected reason when possible.
Use safeExhaustive() for closed unions where operational failures should be returned as values:
const result = await match
.promise(profilePromise)
.with({ ok: true }, (value) => value.profile.name)
.with({ ok: false }, (value) => value.message)
.safeExhaustive()
matchBy(value, path) use cases
matchBy reads a direct key, nested dot path, or tuple path and dispatches by the selected tag. Handlers receive the full input value narrowed by the tag.
Direct keys
const operation = matchBy(cartAction, 'type')
.with('addItem', (action) => ({ type: 'lineItemAdded', sku: action.sku, quantity: action.quantity }))
.with('applyCoupon', (action) => ({ type: 'discountApplied', code: action.code, multiplier: 0.9 }))
.with('clearCart', (action) => ({ type: 'cartCleared', reason: action.reason }))
.exhaustive()
Nested dot paths and tuple paths
const label = matchBy(event, 'meta.type')
.with('click', (event) => `click:${event.meta.x}`)
.with('submit', (event) => `submit:${event.meta.form}`)
.exhaustive()
Use tuple paths for symbol keys or literal path segments that contain dots:
const label = matchBy(event, ['meta', EVENT_KIND])
.with('user', (event) => event.meta.name)
.with('system', (event) => String(event.meta.code))
.exhaustive()
Autocomplete suggests finite tag-like paths. Broad scalar paths such as arbitrary string or number fields remain accepted manually but are not suggested as primary discriminants.
.with(...tags, handler)
Use chained .with(...) for normal application dispatch. One or more tags can share a handler, and variadic .with(tag1, tag2, handler) is the default grouping mechanism for simple same-handler cases.
const status = matchBy(event, 'type')
.with('start', 'resume', (event) => `active:${event.id}`)
.with('stop', (event) => `stopped:${event.reason}`)
.exhaustive()
Keep one-to-one branches as one .with(tag, handler) each. Do not rewrite a readable .with(...) chain into .cases((group) => [...]) only to use group(...) for one repeated handler.
.cases({...}) object maps
Use object maps for compact exhaustive maps when tags are representable as object keys and there are no normalized key collisions.
const auditEvent = matchBy(cartAction, 'type').cases({
addItem: (action) => ({ category: 'inventory', sku: action.sku, quantity: action.quantity }),
applyCoupon: (action) => ({ category: 'pricing', code: action.code, percentOff: action.percentOff }),
clearCart: (action) => ({ category: 'lifecycle', reason: action.reason }),
})
Avoid object maps for null, undefined, or collisions like 1 and '1'; use tuple/grouped cases instead. Avoid bare __proto__: object-literal syntax because JavaScript treats it specially; use computed ['__proto__'], tuple entries, or grouped cases when that tag matters.
.cases((group) => [...]) callback groups
Use callback groups when case-list syntax is materially clearer than chained .with(...): many branches are grouped, cases are generated or reused, tuple/object entries are needed, or callback-local grouped inference is specifically needed. Do not use group(tag, handler) for ordinary one-to-one cases that read better as .with(tag, handler).
const status = matchBy(event, 'type').cases((group) => [
group('start', 'resume', (event) => `active:${event.id}`),
group('stop', 'pause', (event) => `inactive:${event.reason}`),
group('error', 'timeout', (event) => `error:${event.message}`),
])
This form supports single-tag groups, variadic multi-tag groups, and array-form groups. Prefer chained .with(...) when most entries would be single-tag groups.
.cases([...]) tuple/grouped entry arrays
Use entry arrays when cases are generated, need universal tag support, or are easier to read as tuples.
type State =
| { kind: 'ready'; data: string }
| { kind: 'failed'; reason: string }
| { kind: null }
| { kind?: undefined }
declare const state: State
const label = matchBy(state, 'kind').cases([
['ready', (state) => state.data],
[[null, undefined], () => 'empty'],
['failed', (state) => state.reason],
])
Valid entries:
[tag, handler];
[[tag1, tag2], handler];
group(tag, handler);
group(tag1, tag2, ...moreTags, handler);
group(tags, handler).
Inline tuple-entry arrays contextually infer handlers from sibling tags. Partial grouped arrays preserve tag autocomplete while editing grouped tags. Exhaustive grouped .cases([...]) keeps missing-case diagnostics active while the list is incomplete, so callback .cases((group) => [group('a', 'b', handler)]) is the best autocomplete shape for exhaustive grouped cases. Broad runtime arrays are runtime-valid but do not prove exhaustive coverage. Exported group(...) entries are useful for reusable structures but can need explicit handler parameter annotations; use callback .cases((group) => [...]) or .partial((group) => [...]) when you want grouped entries with the strongest annotation-free handler inference.
.partial(...).otherwise(...)
Use .partial(...) when only some tags need special behavior before a fallback.
const response = matchBy(cartAction, 'type')
.partial({
addItem: (action) => ({ type: 'recalculate', cartId: action.cartId, sku: action.sku }),
})
.otherwise((remaining) =>
remaining.type === 'checkout'
? { type: 'reviewTotal', cartId: remaining.cartId, total: remaining.total }
: { type: 'unchanged' },
)
.partial(...) accepts object maps, tuple/grouped entry arrays, and callback-local grouped entries:
const review = matchBy(cartAction, 'type')
.partial([
['addItem', (action) => ({ type: 'inventoryCheck', sku: action.sku, quantity: action.quantity })],
[
['updateQuantity', 'applyCoupon'],
(action) => ({
type: 'pricePreview',
cartId: action.cartId,
subtotal: action.subtotal,
}),
],
])
.otherwise((remaining) =>
remaining.type === 'checkout'
? { type: 'checkoutReview', cartId: remaining.cartId, total: remaining.total }
: { type: 'noReview' },
)
Use .partial((group) => [...]) only for partial tag handling followed by .otherwise(...), and only when the callback group form is clearer than chained .with(...).otherwise(...). partial is not required to share one handler; for simple grouped tags, prefer variadic .with(tag1, tag2, handler). Prefer variadic callback groups like group('addItem', 'updateQuantity', handler) while typing because they provide the best tag autocomplete; array groups like group(['addItem', 'updateQuantity'], handler) remain valid without as const when the grouped tag list reads better.
matchBy.promise(valueOrPromise, path) use cases
matchBy.promise(...) mirrors matchBy(...), but resolves the input internally and returns promises from terminal methods. Pass the promise directly; path, tag, case-map, partial-map, and grouped-case inference all use Awaited<TInput>.
type Order =
| { state: 'pending'; id: string; total: number }
| { state: 'paid'; id: string; total: number; receiptUrl: string }
| { state: 'shipped'; id: string; trackingNumber: string }
| { state: 'cancelled'; id: string; reason: string }
const orders: readonly Order[] = [{ state: 'paid', id: 'order-1', total: 49, receiptUrl: '/receipts/order-1' }]
const fallbackOrder: Order = { state: 'cancelled', id: 'missing', reason: 'not found' }
async function fetchOrder(id: string) {
return orders.find((order) => order.id === id) ?? fallbackOrder
}
const orderView = await matchBy
.promise(fetchOrder('order-1'), 'state')
.with('pending', (order) => ({ screen: 'checkout', orderId: order.id, total: order.total }))
.with('paid', (order) => ({ screen: 'receipt', orderId: order.id, receiptUrl: order.receiptUrl }))
.with('shipped', (order) => ({ screen: 'tracking', orderId: order.id, trackingNumber: order.trackingNumber }))
.with('cancelled', (order) => {
throw new Error(`Order was cancelled: ${order.reason}`)
})
.exhaustive()
Normal terminals reject for input rejection, path-read errors, handler throws/rejections, fallback throws/rejections, and defensive non-exhaustiveness. .otherwise(...) is only a tag fallback; it does not catch input rejection.
Promise-safe terminals mirror match.promise:
const result = await matchBy
.promise(fetchOrder('order-1'), 'state')
.with('cancelled', (order) => ({ screen: 'cancelled', reason: order.reason }))
.safeOtherwise((order) => ({ screen: 'order', orderId: order.id }))
All synchronous matchBy case shapes are available on promise builders; show only the shape needed for the example instead of stacking every feature into one snippet.
group(...) use cases
Prefer variadic .with(...) before reaching for group(...):
const status = matchBy(event, 'type')
.with('start', 'resume', (event) => `active:${event.id}`)
.with('stop', (event) => `stopped:${event.reason}`)
.exhaustive()
Use callback group when case-list syntax is genuinely clearer or needed for generated/reusable/group-heavy case sets:
const status = matchBy(event, 'type').cases((group) => [
group('start', 'resume', (event) => `active:${event.id}`),
group('stop', 'pause', (event) => `inactive:${event.reason}`),
])
Do not use group(...) for one-to-one cases in normal application code. Use exported group(...) for reusable prebuilt groups, especially when handlers do not need narrowed parameters or are explicitly annotated:
const statusCases = [group(['start', 'resume'], () => 'active'), group('stop', () => 'inactive')]
Supported forms:
group(tag, handler) — one tag.
group(tag1, tag2, ...moreTags, handler) — two or more tags.
group(tags, handler) — array/tuple tags; useful when tags read better together.
Array-form groups remain supported and are often more readable because group keeps two arguments. For exhaustiveness, array-form tags must be statically known: inline arrays count as covered tags; broad runtime arrays do not prove coverage.
Pattern helpers
P namespace helpers:
P._, P.any — wildcard helpers that match anything.
P.string, P.number, P.boolean, P.bigint, P.symbol, P.null, P.undefined — primitive helpers.
P.nan, P.finite, P.integer — numeric helpers.
P.regex(regex) — string-only regular expression matching; restores regex.lastIndex.
P.date, P.error, P.regexp — valid Date, Error, and RegExp instance helpers.
P.nullish, P.falsy, P.truthy — JavaScript nullish/truthiness helpers.
P.temporal, P.temporalInstant, P.temporalPlainDate, P.temporalPlainTime, P.temporalPlainDateTime, P.temporalZonedDateTime, P.temporalDuration, P.temporalPlainYearMonth, P.temporalPlainMonthDay — Temporal helpers. They match nothing when globalThis.Temporal is unavailable and do not polyfill Temporal.
P.literal(value) — exact primitive value or object/function/array reference identity matching.
P.union(...patterns) — matches any listed pattern; requires at least one pattern.
P.exclude(pattern) — matches values that do not match the nested pattern; cannot contain selections or collection captures.
P.optional(pattern) — matches an absent object property, undefined, or the nested pattern.
P.array(pattern) — variable-length arrays where every item matches; selections inside are rejected.
P.nonEmptyArray(pattern) — same as P.array(...) but requires at least one item.
P.map(keyPattern, valuePattern) — actual Map instances where every entry matches homogeneous key/value patterns.
P.map([keyPattern, valuePattern], ...) — actual Map instances with distinct required entries; partial by default, exact with P.exact(...).
P.set(valuePattern) — actual Set instances where every value matches one pattern.
P.set(valuePattern, ...moreValuePatterns) — actual Set instances with distinct required values; partial by default, exact with P.exact(...).
P.tuple([...]) — explicit exact tuple pattern.
P.rest(pattern) — remaining tuple items; valid only as the final tuple item.
P.exact(pattern) — deep exact object pattern rejecting enumerable own extra value keys; with required Map/Set mode it rejects extra unconsumed entries/values.
P.when(predicate) — nested predicate or type guard pattern.
P.instanceOf(Constructor) — instanceof pattern for classes/errors.
P.select() — anonymous selection.
P.select(name) — named selection of the current value.
P.select(name, pattern) — named selection after nested validation.
P.collect(name, pattern) — collection capture for repeated containers; handler receives name: T[].
P.record(keyPattern, valuePattern) — plain record-like objects; empty records match.
P.nonEmptyRecord(keyPattern, valuePattern) — plain record-like objects with at least one key.
Named helper exports mirror P helpers:
pWildcard, pAny, pString, pNumber, pBoolean, pBigint, pSymbol, pNull, pUndefined
pNan, pFinite, pInteger
pRegex, pDate, pError, pRegexp, pNullish, pFalsy, pTruthy
pTemporal, pTemporalInstant, pTemporalPlainDate, pTemporalPlainTime, pTemporalPlainDateTime, pTemporalZonedDateTime, pTemporalDuration, pTemporalPlainYearMonth, pTemporalPlainMonthDay
pLiteral, pUnion, pExclude, pOptional
pArray, pNonEmptyArray, pMap, pSet, pTuple, pRest
pExact, pWhen, pInstanceOf, pSelect, pCollect, pRecord, pNonEmptyRecord
Use named helpers when codebases prefer focused imports or want helper usage visible to bundlers.
Use P.regex(regex) instead of hand-written P.when(...) for string regex checks. Use P.nullish only for null | undefined; wrap it in P.optional(...) when an absent object property should also match. Use P.date for valid dates and P.instanceOf(Date) only when invalid Date instances should still match. Temporal helpers depend on runtime constructors supplied by the environment or the application; they are safe to import in projects that do not include TypeScript's ESNext.Temporal lib.
Runtime guards and assertions
isMatching
const isUser = isMatching({ type: 'user', id: P.string })
const users = values.filter(isUser)
if (isMatching({ type: 'user', id: P.string }, payload)) {
payload.id
}
Use isMatching for filters, conditional branches, and non-throwing runtime validation. It supports direct and curried forms.
assertMatching
const form = Object.fromEntries(new URLSearchParams('type=user&id=u1&role=admin'))
assertMatching({ type: 'user', id: P.string, role: P.union('admin', 'member') }, form)
form.id
Use assertMatching at boundaries where mismatch should throw: request bodies, form data, API payloads, webhook events, storage reads, test fixtures, and CLI arguments. A mismatch throws PatternMismatchError.
Error and diagnostic APIs
NonExhaustiveMatchError — thrown by .exhaustive() and exhaustive matchBy(...).cases(...) when runtime data reaches an unhandled branch. Exposes matcher, path, tag, valuePreview, and non-enumerable value.
PatternMismatchError — thrown by assertMatching(...). Exposes valuePreview, patternPreview, and non-enumerable value/pattern.
preview(value) — low-level diagnostic helper from the errors subpath. Prefer error classes in normal app code.
MatchErrorMetadata — metadata interface used by NonExhaustiveMatchError.
Responding to ts-match: diagnostics
ts-match intentionally shapes common invalid usage into readable TypeScript diagnostics whose messages start with ts-match:. When helping a user or fixing generated code:
- Read the
ts-match: message before the surrounding TypeScript overload noise.
- Apply the suggested fix directly.
- Do not add unsafe casts, broad
any, or manual type assertions to bypass the diagnostic.
- Do not replace
matchBy with switch just to silence an error.
- Re-run the typecheck after fixing the modeled problem.
Common diagnostic fixes:
ts-match: match is not exhaustive / ts-match: matchBy is not exhaustive — add missing .with(...) / grouped cases, or use .otherwise(...) only when fallback behavior is intentional.
ts-match: invalid matchBy path — fix the direct key, dot path, or tuple path. Use tuple paths for symbol keys or keys containing ..
ts-match: this matchBy tag cannot occur — remove the impossible tag or correct the path.
ts-match: object-map cases are missing required key(s) — add missing handlers or change to .partial(...).otherwise(...).
ts-match: object-map case contains an extra key — remove the extra key or fix the discriminant path.
ts-match: object-map cases cannot represent null or undefined tags / key collision diagnostics — use tuple-entry cases or callback grouped cases instead of an object map.
ts-match: repeated container patterns cannot contain P.select(...) — move the selection outside P.array(...), P.nonEmptyArray(...), P.record(...), P.nonEmptyRecord(...), P.map(...), or P.set(...); use P.collect(...) for repeated captures.
ts-match: invalid P.collect(...) usage — use P.collect(name, pattern) only inside repeated containers, do not mix it with anonymous P.select(), and do not reuse a named selection name.
ts-match: P.exclude(pattern) cannot contain P.select(...) / P.collect(...) — remove the capture or move it outside the excluded pattern.
ts-match: invalid P.rest(...) usage — use P.rest(...) only as the final tuple pattern item.
If grouped-case handler inference is weak and variadic .with(tag1, tag2, handler) cannot express the needed structure cleanly, prefer callback-local .cases((group) => [...]) or .partial((group) => [...]) so the handler is typed from the active matchBy path. Use variadic callback groups (group('a', 'b', handler)) when editor tag suggestions matter; array-form groups (group(['a', 'b'], handler)) are supported without as const but may not get the same in-array literal completions. Use exported group(...) for reusable groups whose handlers do not need contextual variant inference.
Important limitations
P.array(...), P.nonEmptyArray(...), P.record(...), P.nonEmptyRecord(...), P.map(...), and P.set(...) reject P.select(...) because captures may repeat ambiguously; use P.collect(...) for repeated captures.
P.collect(...) is a matching capture, not a filter. Use P.union(...) for mixed repeated values.
P.exclude(...) cannot contain selections or collection captures.
P.rest(...) is valid only as the final tuple pattern item.
P.record(...) and P.nonEmptyRecord(...) target plain record-like objects, not arrays, class instances, maps, sets, dates, regexps, or primitives.
P.map(...) and P.set(...) target actual Map/Set instances only. They do not match plain objects, entry arrays, arrays, or duck-typed collection-like values.
- Values typed as
ReadonlyMap or ReadonlySet narrow to readonly handler types, but runtime matching still requires actual Map/Set instances.
- Top-level array pairs in
P.map(...) are required-entry clauses. Use P.tuple(...) for homogeneous tuple keys or tuple values.
- Dot paths always mean nesting. Use tuple paths for symbols and literal segments containing dots.
- Object patterns use normal JavaScript property lookup, so getters can run or throw and inherited properties can match.
P.exact(...) rejects object extra keys and required Map/Set extra entries or values, but it is not a cyclic graph matcher.
- Object-map
.cases({...}) cannot represent null, undefined, or normalized key collisions. Avoid bare __proto__: object-literal syntax; use computed ['__proto__'], tuple/grouped entries, or callback grouped cases.
- Standalone exported
group(...) cannot always infer handler parameter types from a later .cases(...) or .partial(...) call. Use callback-local group for annotation-free grouped handlers.
- Temporal helpers do not polyfill
globalThis.Temporal; they match nothing until the runtime or application provides Temporal constructors.
Anti-patterns
- Importing internal files.
- Using undocumented helper aliases.
- Adding casts to force handler types instead of changing the pattern, path, or callback
group shape.
- Using
match.promise(...) or matchBy.promise(...) only because handlers are async while the matched input is already resolved; use plain match(...) / matchBy(...) and await the terminal result instead.
- Awaiting promise-producing sources before
match.promise(...) or matchBy.promise(...) when passing the source directly would keep inference and error handling simpler.
- Using inline
.cases({...}) inside hot loops.
- Converting a simple chained
.with(...) matcher into .cases((group) => [...]) just to group one branch.
- Using
group(tag, handler) for one-to-one cases where .with(tag, handler) is simpler.
- Recommending hoisted case maps that require manual handler annotations as normal user-facing code.
- Using object-map
.cases({...}) for null, undefined, bare __proto__: syntax, or normalized key collisions.
- Selecting inside repeated contexts such as arrays or records instead of using
P.collect(...) for repeated captures.
- Writing examples that are not compiled against the installed package.
- Writing examples that depend on one app's private domain, IPC payloads, or tool/event names instead of generic product/application scenarios.
Validation checklist
Before introducing or modifying ts-match usage:
- Confirm every imported symbol is listed in this skill or README.
- Confirm examples import only from package root or documented subpaths.
- Confirm closed unions use
.exhaustive() or exhaustive .cases(...).
- Confirm simple same-handler cases use variadic
.with(...) before considering callback group(...).
- Confirm
.partial(...) is used only for partial handling plus fallback, not merely to access group(...).
- Confirm promise-backed sources are passed directly to
match.promise or matchBy.promise when the resolved value is primarily consumed by the matcher.
- Confirm safe terminals are used only on promise builders.
- Confirm
safeOtherwise(...) always has a fallback handler.
- Confirm
matchBy.promise(...) path/tag/case/group inference is based on the resolved input type.
- Confirm no unsafe casts, broad
any, internal imports, unsupported helper names, or switch rewrites were added.
- Confirm docs/examples use generic scenarios; include JSX-return examples for UI-state use cases when helpful, without implying a React dependency.
- Compile the affected project examples/tests.
- If editing this library itself, run
pnpm check, pnpm pack:check, and pnpm test.
- If changing public types or overloads, ensure
pnpm test:editor-dx is covered by pnpm check and verify packaged dist/*.d.ts autocomplete when relevant.