NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
npm · #3978 most downloaded on npm
The exhaustive Pattern Matching library for TypeScript.
Last release 11 months ago
26 Oct 2025
Ships fairly regularly
a new release about every 2 months
Most releases are documented
notes for 47 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
6 years old
159 releases · first in 2020
One column per quarter.
To match a Record (an object with consistent key and value types), you can use P.record(keyPattern, valuePattern). It takes a sub-pattern to match aga
P.record patternsTo match a Record<Key, Value> (an object with consistent key and value types), you can use P.record(keyPattern, valuePattern).
It takes a sub-pattern to match against the key, a sub-pattern to match against the value, and will match if all entries in the object
match these two sub-patterns.
import { match, P } from 'ts-pattern';
type Input = Record<string, number>;
const input: Input = {
alice: 100,
bob: 85,
charlie: 92,
};
const output = match(input)
.with(P.record(P.string, P.number), (scores) => `All user scores`)
.with(P.record(P.string, P.string), (names) => `All user names`)
.otherwise(() => '');
console.log(output);
// => "All user scores"
You can also use P.record with a single argument P.record(valuePattern), which assumes string keys:
const userProfiles = {
alice: { name: 'Alice', age: 25 },
bob: { name: 'Bob', age: 30 },
};
const output = match(userProfiles)
.with(
P.record({ name: P.string, age: P.number }),
(profiles) => `User profiles with name and age`
)
.otherwise(() => 'Different format');
console.log(output);
// => "User profiles with name and age"
When using P.select in record patterns, you can extract all keys or all values as arrays:
const data = { a: 1, b: 2, c: 3 };
const keys = match(data)
.with(P.record(P.string.select(), P.number), (keys) => keys)
.otherwise(() => []);
const values = match(data)
.with(P.record(P.string, P.number.select()), (values) => values)
.otherwise(() => []);
console.log(keys); // => ['a', 'b', 'c']
console.log(values); // => [1, 2, 3]
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.8.0...v5.9.0
.narrow() gives you fine-grained control over type narrowing of deeply nested union types during pattern matching.
.narrow() Method for Deep Type Narrowing.narrow() gives you fine-grained control over type narrowing of deeply nested union types during pattern matching.
.narrow()?The .narrow() method allows you to explicitly narrow the input type to exclude all values that have been handled by previous patterns. This is especially useful when working with:
.narrow()By default, TS-Pattern automatically narrows top-level union types as you pattern match. However, for deeply nested types, this narrowing doesn't happen automatically to maintain optimal TypeScript performance. The .narrow() method gives you explicit control over when to perform this more computationally expensive operation.
type Input = { user: { role: 'admin' | 'editor' | 'viewer' } };
declare const input: Input;
const result = match(input)
.with({ user: { role: 'admin' } }, handleAdmin)
.narrow() // Explicitly narrow remaining cases
.with({ user: { role: 'editor' } }, handleEditor)
.narrow() // Narrow again if needed
.otherwise((remaining) => {
// remaining.user.role is now precisely 'viewer'
handleViewer(remaining);
});
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.7.1...v5.8.0
This new release fixes the following bug in exhaustiveness checking when matching on optional properties:
This new release fixes the following bug in exhaustiveness checking when matching on optional properties:
type Input = { type?: 'one' } | { type: 'two' };
const f1 = (input: Input) =>
match(input)
.with({ type: 'one' }, () => {})
.with({ type: 'two' }, () => {})
.exhaustive(); // shouldn't type-check, but does 👎
const f2 = (input: Input) =>
match(input)
.with({ type: 'one' }, () => {})
.with({ type: 'two' }, () => {})
.with({ type: undefined }, () => {}) // <- the type key needs to be present.
.exhaustive(); // shouldn't type-check, but does 👎
These two cases don't type check anymore. They fail with a NonExhaustiveError<{ type?: undefined; }>. To fix it, you should do:
type Input = { type?: 'one' } | { type: 'two' };
const f = (input: Input) =>
match(input)
.with({ type: 'one' }, () => {})
.with({ type: 'two' }, () => {})
.with({ type: P.optional(undefined) }, () => {}) // <- the type property may not be there
.exhaustive(); // ✅
This is a purely type-level change, the runtime behavior is still the same.
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.7.0...v5.7.1
By default, .exhaustive() will throw an error if the input value wasn't handled by any .with(...) clause. This should only happen if your types are in
By default, .exhaustive() will throw an error if the input value wasn't handled by any .with(...) clause. This should only happen if your types are incorrect.
It is possible to pass your own handler function as a parameter to decide what should happen if an unexpected value has been received. You can for example throw your own custom error:
match(...)
.with(...)
.exhaustive((unexpected: unknown) => {
throw MyCustomError(unexpected);
})
Or log an error and return a default value:
match<string, number>(...)
.with(P.string, (str) => str.length)
.exhaustive((notAString: unknown) => {
console.log(`received an unexpected value: ${notAString}`);
return 0;
})
isMatchingisMatching didn't have full feature parity with match in terms of type narrowing, but now does.
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.6.2...v5.7.0
fix(isMatching): Fix non-object patterns by @gvergnaud in https://github.com/gvergnaud/ts-pattern/pull/307
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.6.1...v5.6.2
fix(isMatching): Allow unknown properties in pattern by @gvergnaud in https://github.com/gvergnaud/ts-pattern/pull/305
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.6.0...v5.6.1
This release contains two changes:
This release contains two changes:
It used to be possible to pass a pattern than could never match to isMatching. The new version checks that the provide pattern does match the value in second parameter:
type Pizza = { type: 'pizza'; topping: string };
type Sandwich = { type: 'sandwich'; condiments: string[] };
type Food = Pizza | Sandwich;
const fn = (food: Pizza | Sandwich) => {
if (isMatching({ type: 'oops' }, food)) {
// 👆 used to type-check, now doesn't!
}
}
P.infer as an inference pointWhen using P.infer<Pattern> to type a function argument, like in the following example:
const getWithDefault = <T extends P.Pattern>(
input: unknown,
pattern: T,
defaultValue: P.infer<T> // 👈
): P.infer<T> =>
isMatching(pattern, input) ? input : defaultValue
TypeScript could get confused and find type errors in the wrong spot:
const res = getWithDefault(null, { x: P.string }, 'oops')
// 👆 👆 type error should be here
// but it's here 😬
This new version fixes this problem.
P.infer and isMatching by @gvergnaud in https://github.com/gvergnaud/ts-pattern/pull/302Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.5.0...v5.6.0
Export Pattern types in package.json by @GGomez99 in https://github.com/gvergnaud/ts-pattern/pull/292 closing issue https://github.com/gvergnaud/ts-pa
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.4.0...v5.5.0
This release brings a significant perf improvement to exhaustiveness checking, which led to a ~16% decrease in the time to type-check the full test su
This release brings a significant perf improvement to exhaustiveness checking, which led to a ~16% decrease in the time to type-check the full test suite of TS-Pattern:
| Category | Before | After | Evolution (%) |
|---|---|---|---|
| Instantiations | 6,735,991 | 4,562,378 | -32.33% |
| Memory used | 732,233K | 746,454K | 1.95% |
| Assignability cache size | 209,959 | 205,926 | -1.92% |
| Identity cache size | 28,093 | 28,250 | 0.56% |
| Check time | 5.78s | 4.83s | -16.44% |
InvertPatternForExcludeInternal to work with readonly array by @changwoolab in https://github.com/gvergnaud/ts-pattern/pull/284Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.3.1...v5.4.0
Symbols used to be ignored in object patterns. They are now taken into account:
Symbols used to be ignored in object patterns. They are now taken into account:
const symbolA = Symbol('symbol-a');
const symbolB = Symbol('symbol-b');
const obj = { [symbolA]: { [symbolB]: 'foo' } };
if (isMatching({ [symbolA]: { [symbolB]: 'bar' } }, obj)) {
// 👆 Used to return true, now returns false!
// Since TS-Pattern wasn't reading symbols, this pattern used to be equivalent
// to the `{}` pattern that matches any value except null and undefined.
}
.exhaustive now throws a custom errorPeople have expressed the need to differentiate runtime errors that .exhaustive() might throw when the input is of an unexpected type from other runtime errors that could have happened in the same match expression. It's now possible with err instanceof NonExhaustiveError:
import { match, P, NonExhaustiveError } from 'ts-pattern';
const fn = (input: string | number) => {
return match(input)
.with(P.string, () => "string!")
.with(P.number, () => "number!")
.exhaustive()
}
try {
fn(null as string) // 👈 💥
} catch (e) {
if (e instanceof NonExhaustiveError) {
// The input was invalid
} else {
// something else happened
}
}
ExhaustiveError when no matched pattern by @adamhamlin in https://github.com/gvergnaud/ts-pattern/pull/270Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.2.0...v5.3.1
Nothing published for this version
P.string.length(len) matches strings with exactly len characters.
P.string.length(n) patternP.string.length(len) matches strings with exactly len characters.
const fn = (input: string) =>
match(input)
.with(P.string.length(2), () => '🎉')
.otherwise(() => '❌');
console.log(fn('ok')); // logs '🎉'
P.when patterns code example by @grigorischristainas in https://github.com/gvergnaud/ts-pattern/pull/260Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.1.2...v5.2.0
When combining P.nonNullable and P.nullish, you should get an exhaustive pattern matching expression, but the following case was incorrectly considere
When combining P.nonNullable and P.nullish, you should get an exhaustive pattern matching expression, but the following case was incorrectly considered non-exhaustive:
declare const input: {
nested: string | number | null | undefined;
};
const res = match(input)
.with({ nested: P.nonNullable }, (x) => {/* ... */})
.with({ nested: P.nullish }, (x) => {/* ... */})
// should type-check
.exhaustive();
This is fixed now.
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.1.1...v5.1.2
Fix(P.nonNullable): narrowing of unions of objects by @gvergnaud in https://github.com/gvergnaud/ts-pattern/pull/237
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.1.0...v5.1.1
Nothing published for this version
Add a new P.nonNullable pattern that will match any value except null or undefined.
P.nonNullable wildcardAdd a new P.nonNullable pattern that will match any value except null or undefined.
import { match, P } from 'ts-pattern';
const input = null;
const output = match<number | null | undefined>(input)
.with(P.nonNullable, () => 'it is a number!')
.otherwise(() => 'it is either null or undefined!');
console.log(output);
// => 'it is either null or undefined!'
Closes #60 #154 #190 and will be a work-around for #143.
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.0.8...v5.1.0
This release includes type narrowing improvement to isMatching when used in its curried form:
This release includes type narrowing improvement to isMatching when used in its curried form:
type Pizza = { type: 'pizza', topping: string };
type Sandwich = { type: 'sandwich', condiments: string[] }
type Food = Pizza | Sandwich;
declare const food: Food
const isPizza = isMatching({ type: 'pizza' })
if (isPizza(food)) {
x // Used to infer `food` as `Food`, no infers `Pizza`
}
This also improves type checking performance for complex patterns and fixes a small bug in the ES5 build of TS-Pattern.
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.0.6...v5.0.8
Nothing published for this version
Exhaustive matching fix for read only arrays https://github.com/gvergnaud/ts-pattern/issues/206
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.0.5...v5.0.6
The P module was mistakenly exposing some pattern methods that were intended to be namespaced by type. This release fixes this problem.
The P module was mistakenly exposing some pattern methods that were intended to be namespaced by type. This release fixes this problem.
If you happened to use on of those following methods, here is where to find them now:
- P.between
+ P.number.between
- P.lt
+ P.number.lt
- P.gt
+ P.number.gt
- P.lte
+ P.number.lte
- P.gte
+ P.number.gte
- P.int
+ P.number.int
- P.finite
+ P.number.finite
- P.positive
+ P.number.positive
- P.negative
+ P.number.negative
- P.betweenBigInt
+ P.bigint.between
- P.ltBigInt
+ P.bigint.lt
- P.gtBigInt
+ P.bigint.gt
- P.lteBigInt
+ P.bigint.lte
- P.gteBigInt
+ P.bigint.gte
- P.positiveBigInt
+ P.bigint.positive
- P.negativeBigInt
+ P.bigint.negative
🐛 fix: Accept branded primitive types as patterns by @gvergnaud in https://github.com/gvergnaud/ts-pattern/pull/180
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.0.3...v5.0.4
🐛 fix: Allow re-exporting patterns from ES Modules by @gvergnaud in https://github.com/gvergnaud/ts-pattern/pull/175
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.0.2...v5.0.3
Nothing published for this version
Nothing published for this version
Nothing published for this version
fix(P.infer): Fix inference of arrays containing tuples in https://github.com/gvergnaud/ts-pattern/commit/c52af6a2fa75ac37d8b8e1a1b9832445e4c4f580
Symbol.for to make sure two concurrent versions of ts-pattern are compatible with one-another in https://github.com/gvergnaud/ts-pattern/commit/d6d2e23ca98dbbba02b2ee4778c784cc67b858bdFull Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.0.0...v5.0.2
Nothing published for this version
In the previous version of TS-Pattern, no code would execute until you called .exhaustive() or .otherwise(...). For example, in the following code blo
.with is now evaluated eagerlyIn the previous version of TS-Pattern, no code would execute until you called .exhaustive() or .otherwise(...). For example, in the following code block, nothing would be logged to the console or thrown:
// TS-Pattern v4
type Input = { type: 'ok'; value: number } | { type: 'error'; error: Error };
// We don't call `.exhaustive`, so handlers don't run.
function someFunction(input: Input) {
match(input)
.with({ type: 'ok' }, ({ value }) => {
console.log(value);
})
.with({ type: 'error' }, ({ error }) => {
throw error;
});
}
someFunction({ type: 'ok', value: 42 }); // nothing happens
In TS-Pattern v5, however, the library will execute the matching handler as soon as it finds it:
// TS-Pattern v5
someFunction({ type: 'ok', value: 42 }); // logs "42" to the console!
Handlers are now evaluated eagerly instead of lazily. In practice, this shouldn't change anything as long as you always finish your pattern matching expressions by either .exhaustive or .otherwise.
Matching Set and Map instances using .with(new Set(...)) and .with(new Map(...)) is no longer supported. If you want to match specific sets and maps, you should now use the P.map(keyPattern, valuePattern) and P.set(valuePattern) patterns:
- import { match } from 'ts-pattern';
+ import { match, P } from 'ts-pattern';
const someFunction = (value: Set<number> | Map<string, number>) =>
match(value)
- .with(new Set([P.number]), (set) => `a set of numbers`)
- .with(new Map([['key', P.number]]), (map) => `map.get('key') is a number`)
+ .with(P.set(P.number), (set) => `a set of numbers`)
+ .with(P.map('key', P.number), (map) => `map.get('key') is a number`)
.otherwise(() => null);
P.set(subpattern) should match all values in the set.P.map(keyPattern, subpattern) should only match the values matching keyPattern for the whole P.map(..) pattern to match the input.TS-Pattern v5's major addition is the ability to chain methods to narrow down the values matched by primitive patterns, like P.string or P.number.
Since a few examples is worth a thousand words, here are a few ways you can use chainable methods:
const example = (position: { x: number; y: number }) =>
match(position)
.with({ x: P.number.gte(100) }, (value) => '🎮')
.with({ x: P.number.between(0, 100) }, (value) => '🎮')
.with(
{
x: P.number.positive().int(),
y: P.number.positive().int(),
},
(value) => '🎮'
)
.otherwise(() => 'x or y is negative');
Here is the full list of number methods:
P.number.between(min, max): matches numbers between min and max.P.number.lt(max): matches numbers smaller than max.P.number.gt(min): matches numbers greater than min.P.number.lte(max): matches numbers smaller than or equal to max.P.number.gte(min): matches numbers greater than or equal to min.P.number.int(): matches integers.P.number.finite(): matches all numbers except Infinity and -InfinityP.number.positive(): matches positive numbers.P.number.negative(): matches negative numbers.const example = (query: string) =>
match(query)
.with(P.string.startsWith('SELECT'), (query) => `selection`)
.with(P.string.endsWith('FROM user'), (query) => `👯♂️`)
.with(P.string.includes('*'), () => 'contains a star')
// Methods can be chained:
.with(P.string.startsWith('SET').includes('*'), (query) => `🤯`)
.exhaustive();
Here is the full list of string methods:
P.string.startsWith(str): matches strings that start with str.P.string.endsWith(str): matches strings that end with str.P.string.minLength(min): matches strings with at least min characters.P.string.maxLength(max): matches strings with at most max characters.P.string.includes(str): matches strings that contain str.P.string.regex(RegExp): matches strings if they match this regular expression.Some methods are available for all primitive type patterns:
P.{..}.optional(): matches even if this property isn't present on the input object.P.{..}.select(): injects the matched value into the handler function.P.{..}.and(pattern): matches if the current pattern and the provided pattern match.P.{..}.or(pattern): matches if either the current pattern or the provided pattern match.const example = (value: unknown) =>
match(value)
.with(
{
username: P.string,
displayName: P.string.optional(),
},
() => `{ username:string, displayName?: string }`
)
.with(
{
title: P.string,
author: { username: P.string.select() },
},
(username) => `author.username is ${username}`
)
.with(
P.instanceOf(Error).and({ source: P.string }),
() => `Error & { source: string }`
)
.with(P.string.or(P.number), () => `string | number`)
.otherwise(() => null);
With TS-Pattern, you are now able to create array (or more accurately tuple) pattern with a variable number of elements:
const example = (value: unknown) =>
match(value)
.with(
// non-empty list of strings
[P.string, ...P.array(P.string)],
(value) => `value: [string, ...string[]]`
)
.otherwise(() => null);
Array patterns that include a ...P.array are called variadic tuple patterns. You may only have a single ...P.array, but as many fixed-index patterns as you want:
const example = (value: unknown) =>
match(value)
.with(
[P.string, P.string, P.string, ...P.array(P.string)],
(value) => `value: [string, string, string, ...string[]]`
)
.with(
[P.string, P.string, ...P.array(P.string)],
(value) => `value: [string, string, ...string[]]`
)
.with([], (value) => `value: []`)
.otherwise(() => null);
Fixed-index patterns can also be set after the ...P.array variadic, or on both sides!
const example = (value: unknown) =>
match(value)
.with(
[...P.array(P.number), P.string, P.number],
(value) => `value: [...number[], string, number]`
)
.with(
[P.boolean, ...P.array(P.string), P.number, P.symbol],
(value) => `value: [boolean, ...string[], number, symbol]`
)
.otherwise(() => null);
Lastly, argument of P.array is now optional, and will default to P._, which matches anything:
const example = (value: unknown) =>
match(value)
// 👇
.with([P.string, ...P.array()], (value) => `value: [string, ...unknown[]]`)
.otherwise(() => null);
.returnTypeIn TS-Pattern v4, the only way to explicitly set the return type of your match expression is to set the two <Input, Output> type parameters of match:
// TS-Pattern v4
match<
{ isAdmin: boolean; plan: 'free' | 'paid' }, // input type
number // return type
>({ isAdmin, plan })
.with({ isAdmin: true }, () => 123)
.with({ plan: 'free' }, () => 'Oops!');
// ~~~~~~ ❌ not a number.
the main drawback is that you need to set the input type explicitly too, even though TypeScript should be able to infer it.
In TS-Pattern v5, you can use the .returnType<Type>() method to only set the return type:
match({ isAdmin, plan })
.returnType<number>() // 👈 new
.with({ isAdmin: true }, () => 123)
.with({ plan: 'free' }, () => 'Oops!');
// ~~~~~~ ❌ not a number.
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v4.3.0...v5.0.0
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
TS-Pattern now fully supports moduleResolution: node16, with both ES and CommonJS modules. This resolves the long standing issue number #110. Special
TS-Pattern now fully supports moduleResolution: node16, with both ES and CommonJS modules. This resolves the long standing issue number #110. Special thanks to @Andarist and @frankie303 for helping me understand and fix this issue ❤️
Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v4.2.2...v4.3.0
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Issue #142: Fixes a type inference bug when the input type only has optional properties. commit 3c36992
This release fixes inference of P.array when the input is a readonly array (issue #148)
This release fixes inference of P.array when the input is a readonly array (issue #148)
declare const input: readonly {
readonly title: string;
readonly content: string;
}[];
const output = match(input)
.with(
P.array({ title: P.string, content: P.string }),
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Used to error, now works
(posts) => 'a list of posts!'
)
.otherwise(() => 'something else');
When using match with an inline array, it will now infer its type as tuple automatically, even when not using as const. This means that exhaustiveness
match and .withWhen using match with an inline array, it will now infer its type as tuple automatically, even when not using as const. This means that exhaustiveness checking will also improve in this case:
function f(a: boolean, b: boolean) {
// infered as `[boolean, boolean]`
return (
match([a, b])
// we can pattern match on all cases
.with([true, true], () => false)
.with([false, true], () => true)
.with([true, false], () => true)
.with([false, false], () => false)
// ✅ Failed in TS-pattern v4.1 but works in v4.2!
.exhaustive()
);
}
.with(...)Thanks to the help of @Andarist, this release fixes a long-standing issue of .with.
Until now, patterns like P.array, P.union or P.when didn't have proper type inference when used in .with() directly. Here are a few behaviors that use to be incorrect and work now:
match<'a' | 'b'>('a')
.with(P.union('this is wrong'), x => x)
// ~~~~~~~~~~~~~~~
// ❌ no longer type-check in v4.2
.otherwise(x => x)
match<'a' | 'b'>('a')
.with(P.array(123), x => x)
// ~~~
// ❌ no longer type-check in v4.2
.otherwise(x => x)
match<'a' | 'b'>('a')
.with(P.when((x) => true), x => x)
// 👆
// used to be of type `unknown`, now `'a' | 'b'`
.otherwise(x => x)
This also fixes the following issue: https://github.com/gvergnaud/ts-pattern/issues/140
Nothing published for this version
When using P.not with a literal value like P.not(2), Exhaustive checking was mistakenly considering that all numbers had been handled, even though 2 i
P.notWhen using P.not with a literal value like P.not(2), Exhaustive checking was mistakenly considering that all numbers had been handled, even though 2 isn't. This was causing this code to erroneously type-check:
match<number>(input)
.with(P.not(10), () => 'not video of 10 seconds.')
.exhaustive() // This type-checked even though the value `10` isn't handled
This new patch version fixes this bug.
Exhaustive pattern-matching expressions where the input is a primitive (like number) and the pattern is a negation of a literal number (like P.not(2)) are no longer considered exhaustive:
match<number>(1)
.with(P.not(2), () => 'not 2')
.with(2, () => '2')
.exhaustive(); // ❌ `number` isn't handled
Technically, this expression is exhaustive but there is no easy way to type-check it is without negated types (https://github.com/microsoft/TypeScript/pull/29317), so this is an expected false-positive for now.
Exhaustive checking works as expected when the pattern and the input are primitive types:
match<number>(1)
.with(P.not(P.number), () => 'not 2')
.with(P.number, () => '2')
.exhaustive(); // ✅
And when the pattern and the input are literal types:
match<1 | 2>(1)
.with(P.not(2), () => '1')
.with(2, () => '2')
.exhaustive(); // ✅
Nothing published for this version
## Bug fixes - #134 TS-Pattern v4.1.2 was incompatible with older versions of TS because it was using a feature introduced in TS 4.7. The code has bee
With the current version of ts-pattern, nothing prevents you from writing .with clauses that will never match any input because the case has already b
.with clause inherit narrowing from previous clausesWith the current version of ts-pattern, nothing prevents you from writing .with clauses that will never match any input because the case has already been handled in a previous close:
type Plan = 'free' | 'pro' | 'premium';
const welcome = (plan: Plan) =>
match(plan)
.with('free', () => 'Hello free user!')
.with('pro', () => 'Hello pro user!')
.with('pro', () => 'Hello awesome user!')
// 👆 This will never match!
// We should exclude "pro"
// from the input type to
// reject duplicated with clauses.
.with('premium', () => 'Hello premium user!')
.exhaustive()
Initially, I was reluctant to narrow the input type on every call of .with because of type checking performance. TS-Pattern's exhaustive checking is pretty expensive because it not only narrows top-level union types, but also nested ones. In order to make that work, TS-Pattern needs to distribute nested union types when they are matched by a pattern, which can sometimes generate large unions which are more expensive to match.
I ended up settling on a more modest approach, which turns out to have great performance: Only narrowing top level union types. This should cover 80% of cases, including the aforementioned one:
type Plan = 'free' | 'pro' | 'premium';
const welcome = (plan: Plan) =>
match(plan)
.with('free', () => 'Hello free user!')
.with('pro', () => 'Hello pro user!')
.with('pro', () => 'Hello awesome user!')
// ^ ❌ Does not type-check in TS-Pattern v4.1!
.with('premium', () => 'Hello premium user!')
.exhaustive()
Narrowing will work on unions of literals, but also discriminated unions of objects:
type Entity =
| { type: 'user', name: string }
| { type: 'org', id: string };
const f = (entity: Entity) =>
match(entity)
.with({ type: 'user' }, () => 'user!')
.with({ type: 'user' }, () => 'user!')
// ^ ❌ Does not type-check in TS-Pattern v4.1!
.with({ type: 'org' }, () => 'org!')
.exhaustive()
It also works with tuples, and any other union of data structures:
type Entity =
| [type: 'user', name: string]
| [type: 'org', id: string]
const f = (entity: Entity) =>
match(entity)
.with(['user', P.any], () => 'user!')
.with(['user', P.any], () => 'user!')
// ^ ❌ Does not type-check in TS-Pattern v4.1!
.with(['org', P.any], () => 'org!')
.exhaustive()
It works with any patterns, including wildcards:
type Entity =
| [type: 'user', name: string]
| [type: 'org', id: string]
const f = (entity: Entity) =>
match(entity)
.with(P.any, () => 'user!') // catch all
.with(['user', P.any], () => 'user!')
// ^ ❌ Does not type-check in TS-Pattern v4.1!
.with(['org', P.any], () => 'org!')
// ^ ❌ Does not type-check in TS-Pattern v4.1!
.exhaustive()
This won't prevent you from writing duplicated clauses in case the union you're matching is nested:
type Plan = 'free' | 'pro' | 'premium';
type Role = 'viewer' | 'contributor' | 'admin';
const f = (plan: Plan, role: Role) =>
match([plan, role] as const)
.with(['free', 'admin'], () => 'free admin')
.with(['pro', P.any], () => 'all pros')
.with(['pro', 'admin'], () => 'admin pro')
// ^ this unfortunately still type-checks
.otherwise(() => 'other users!')
.otherwise's input also inherit narrowingThe nice effect of refining the input value on every .with clause is that .otherwise also get a narrowed input type:
type Plan = 'free' | 'pro' | 'premium';
const welcome = (plan: Plan) =>
match(plan)
.with('free', () => 'Hello free user!')
.otherwise((input) => 'pro or premium')
// 👆 input is inferred as `'pro' | 'premium'`
Type-checking performance is generally better, with a 29% reduction of type instantiation and a 17% check time improvement on my benchmark:
| description | before | after | delta |
|---|---|---|---|
| Files | 181 | 181 | 0% |
| Lines of Library | 28073 | 28073 | 0% |
| Lines of Definitions | 49440 | 49440 | 0% |
| Lines of TypeScript | 11448 | 11516 | 0.59% |
| Nodes of Library | 119644 | 119644 | 0% |
| Nodes of Definitions | 192409 | 192409 | 0% |
| Nodes of TypeScript | 57791 | 58151 | 0.62% |
| Identifiers | 120063 | 120163 | 0.08% |
| Symbols | 746269 | 571935 | -23.36% |
| Types | 395519 | 333052 | -15.79% |
| Instantiations | 3810512 | 2670937 | -29.90% |
| Memory used | 718758K | 600076K | -16.51% |
| Assignability cache size | 339114 | 311641 | -8.10% |
| Identity cache size | 17071 | 17036 | -0.20% |
| Subtype cache size | 2759 | 2739 | -0.72% |
| Strict subtype cache size | 2544 | 1981 | -22.13% |
| I/O Read time | 0.01s | 0.01s | 0% |
| Parse time | 0.28s | 0.28s | 0% |
| ResolveModule time | 0.01s | 0.02s | 100% |
| ResolveTypeReference time | 0.01s | 0.01s | 0% |
| Program time | 0.34s | 0.34s | 0% |
| Bind time | 0.13s | 0.14s | 7.69% |
| Check time | 5.28s | 4.37s | -17.23% |
| Total time | 5.75s | 4.85s | -15.65% |
package.json exports have been updated to provide a default export for build systems that read neither import nor require.Nothing published for this version
Nothing published for this version
Update P.instanceOf to accept not only classes but also abstract classes. Related issue, https://github.com/gvergnaud/ts-pattern/commit/000927ca441e58
P.instanceOf to accept not only classes but also abstract classes. Related issue, https://github.com/gvergnaud/ts-pattern/commit/000927ca441e58403a8495ddb49eb47828f5d1bb https://github.com/gvergnaud/ts-pattern/commit/ebeb39ba9956596602c34fd5e067db164b70f805abstract class A {}
class B extends A {}
class C extends A {}
const object = new B() as B | C;
match(object)
.with(P.instanceOf(A), a => ...) // a: B | C
// ^
// ✅ This type-checks now!
.exhaustive()
Nothing published for this version
This release adds the ./package.json file to exported files (PR by @zoontek).
This release adds the ./package.json file to exported files (PR by @zoontek).
This fixes https://github.com/nodejs/node/issues/33460 Without it, it breaks require.resolve, used by a lot of tooling (Rollup, React native CLI, etc)
When nesting P.array() and P.select(), the handler function used to receive undefined instead of an empty array when the input array was empty. Now it
Fixes:
P.array() and P.select(), the handler function used to receive undefined instead of an empty array when the input array was empty. Now it received an empty array as expected:match([])
.with(P.array({ name: P.select() }), (names) => names) /* names has type `string[]` and value `[]` */
// ...
.exhaustive()
[]) when matching on a value of type unknown. This has been fixed.const f = (x: unknown) => match(x).with([], () => "this is an empty array!").otherwise(() => "?")
Commits:
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →