Releases: gvergnaud/ts-pattern
v4.1.2
Make subsequent .with
clause inherit narrowing from previous clauses
Problem
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 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()
Approach
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()
Examples of invalid cases that no longer type check:
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()
Examples of invalid cases that still type check:
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 narrowing
The 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'`
Perf
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% |
Other changes
- TS-Pattern's
package.json
exports have been updated to provide a default export for build systems that read neitherimport
norrequire
.
v4.0.6
Bug fixes
- Update
P.instanceOf
to accept not only classes but also abstract classes. Related issue, 000927c ebeb39b
abstract 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()
v4.0.5
This release adds the ./package.json file to exported files (PR by @zoontek).
This fixes nodejs/node#33460
Without it, it breaks require.resolve, used by a lot of tooling (Rollup, React native CLI, etc)
v4.0.4
Fixes:
- When nesting
P.array()
andP.select()
, the handler function used to receiveundefined
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()
- The types used to forbid using an empty array pattern (
[]
) when matching on a value of typeunknown
. This has been fixed.
const f = (x: unknown) => match(x).with([], () => "this is an empty array!").otherwise(() => "?")
Commits:
v4.0.2
Patch release containing a few runtime performance improvements:
- Use a
Builder
class internally in match expression to rely on prototypal inheritance instead of defining method every time.with
is called. - Handle the
.with(pattern, handler)
case separately from.with(...pattern, handler)
to avoid iterating on params and make it faster.
✨ v4.0.1 ✨
⚠️ Breaking changes
Imports
type-specific wildcard patterns have moved from __.<pattern>
to a new Pattern
qualified module, also exported as P
by ts-pattern.
- import { match, __ } from 'ts-pattern';
+ import { match, Pattern } from 'ts-pattern';
const toString = (value: string | number) =>
match(value)
- .with(__.string, (v) => v)
- .with(__.number, (v) => `${v}`)
+ .with(Pattern.string, (v) => v)
+ .with(Pattern.number, (v) => `${v}`)
.exhaustive();
or
- import { match, __ } from 'ts-pattern';
+ import { match, P } from 'ts-pattern';
const toString = (value: string | number) =>
match(value)
- .with(__.string, (v) => v)
- .with(__.number, (v) => `${v}`)
+ .with(P.string, (v) => v)
+ .with(P.number, (v) => `${v}`)
.exhaustive();
__
The top level __
export was moved to P._
and P.any
:
- import { match, __ } from 'ts-pattern';
+ import { match, P } from 'ts-pattern';
const toString = (value: string | number) =>
match(value)
- .with(__, (v) => `${v}`)
+ .with(P._, (v) => `${v}`)
// OR
+ .with(P.any, (v) => `${v}`)
.exhaustive();
select()
, not()
, when()
Function to create patterns have been moved to the P
module.
- import { match, select, not, when } from 'ts-pattern';
+ import { match, P } from 'ts-pattern';
const toString = (value: number) =>
match(value)
- .with({ prop: select() }, (v) => `${v}`)
+ .with({ prop: P.select() }, (v) => `${v}`)
- .with({ prop: not(10) }, (v) => `${v}`)
+ .with({ prop: P.not(10) }, (v) => `${v}`)
- .with({ prop: when((x) => x < 5) }, (v) => `${v}`)
+ .with({ prop: P.when((x) => x < 5) }, (v) => `${v}`)
.exhaustive();
Pattern
type
the Pattern
type which used to be exported at the toplevel is now accessible at P.Pattern
.
- import { match, Pattern } from 'ts-pattern';
+ import { match, P } from 'ts-pattern';
- const pattern: Pattern<number> = P.when(x => x > 2);
+ const pattern: P.Pattern<number> = P.when(x => x > 2);
list patterns
The syntax for matching on a list of elements with an unknown length has changed from [subpattern]
to P.array(subpattern)
.
Example:
- import { match, __ } from 'ts-pattern';
+ import { match, P } from 'ts-pattern';
const parseUsers = (response: unknown) =>
match(response)
- .with({ data: [{ name: __.string }] }, (users) => users)
+ .with({ data: P.array({ name: P.string }) }, (users) => users)
.otherwise(() => []);
Now [subpattern]
matches arrays with 1 element in them. This is more consistent with native language features, like destructuring assignement and is overall more intuitive. This will resolve #69, #62 and #46.
NaN
The __.NaN
pattern has been replaced by simply using the NaN value in the pattern:
match<number>(NaN)
- .with(__.NaN, () => "this is not a number")
+ .with(NaN, () => "this is not a number")
.otherwise((n) => n);
⭐️ New features ⭐️
Here is the list of all new features which have been added in TS-Pattern v4.
Arrays and unary tuples
P.array(pattern)
To match an array of elements, you can now use P.array
:
import { match, P } from 'ts-pattern';
const responsePattern = {
data: P.array({
id: P.string,
post: P.array({
title: P.string,
content: P.string,
}),
}),
};
fetchSomething().then((value: unknown) =>
match(value)
.with(responsePattern, (value) => {
// value: { data: { id: string, post: { title: string, content: string }[] }[] }
return value;
})
.otherwise(() => {
throw new Error('unexpected response');
})
);
Optional object properties
P.optional(pattern)
If you want one of the keys of your pattern to be optional, you can now use P.optional(subpattern)
.
If you P.select()
something in an optional pattern, it's type will be infered as T | undefined
.
import { match, P } from 'ts-pattern';
const doSomethingWithUser = (user: User | Org) =>
match(user)
.with(
{
type: 'user',
detail: {
bio: P.optional(P.string),
socialLinks: P.optional({
twitter: P.select(),
}),
},
},
(twitterLink, value) => {
// twitterLink: string | undefined
/**
* value.detail: {
* bio?: string,
* socialLinks?: {
* twitter: string
* }
* }
**/
}
)
.otherwise(() => {
throw new Error('unexpected response');
});
Union & intersection patterns
P.union(...patterns)
and P.intersection(...patterns)
combine several patterns into a single one, either by checking that one of them match the input (p.union
) or all of them match it (P.intersection
).
P.union(...patterns)
type Input =
| { type: 'a'; value: string }
| { type: 'b'; value: number }
| {
type: 'c';
value:
| { type: 'd'; value: boolean }
| { type: 'e'; value: string[] }
| { type: 'f'; value: number[] };
};
const f = (input: Input) =>
match(input)
.with(
{ type: P.union('a', 'b') },
// x: { type: 'a'; value: string } | { type: 'b'; value: number }
(x) => 'branch 1'
)
.with(
// P.union can take any subpattern:
{
type: 'c',
value: { value: P.union(P.boolean, P.array(P.string)) },
},
(x) => 'branch 2' // x.value.value: boolean | string[]
)
.with({ type: 'c', value: { type: 'f' } }, () => 'branch 3')
.exhaustive();
P.intersection(...patterns)
class A {
constructor(public foo: 'bar' | 'baz') {}
}
class B {
constructor(public str: string) {}
}
const f = (input: { prop: A | B }) =>
match(input)
.with(
{ prop: P.intersection(P.instanceOf(A), { foo: 'bar' }) },
// prop: A & { foo: 'bar' }
({ prop }) => 'branch 1'
)
.with(
{ prop: P.intersection(P.instanceOf(A), { foo: 'baz' }) },
// prop: A & { foo: 'baz' }
({ prop }) => 'branch 2'
)
.with(
{ prop: P.instanceOf(B) },
// prop: B
({ prop }) => 'branch 3'
)
.exhaustive();
Select with sub pattern
P.select()
now can take a subpattern and match only what the subpattern matches:
type Img = { type: 'img'; src: string };
type Text = { type: 'text'; content: string; length: number };
type User = { type: 'user'; username: string };
type Org = { type: 'org'; orgId: number };
const post = (input: { author: User | Org; content: Text | Img }) =>
match(input)
.with(
{ author: P.select({ type: 'user' }) },
// user: User
(user) => {}
)
.with(
{
// This also works with named selections
author: P.select('org', { type: 'org' }),
content: P.select('text', { type: 'text' }),
},
// org: Org, text: Text
({ org, text }) => {}
)
.otherwise(() => {
// ...
});
Infer the matching types from a pattern
P.infer<typeof pattern>
TS-Pattern is pretty handy for parsing unknown payloads like HTTP responses. You can write a pattern for the shape you are expecting, and then use isMatching(pattern, response)
to make sure the response has the correct shape.
One limitation TS-Pattern had in its previous version was that it did not provide a way to get the TypeScript type of the value a given pattern matches. This is what P.infer<typeof pattern>
does :)
const postPattern = {
title: P.string,
description: P.optional(P.string),
content: P.string,
likeCount: P.number,
};
type Post = P.infer<typeof postPattern>;
// Post: { title: string, description?: string, content: string, likeCount: number }
const userPattern = {
name: P.string,
postCount: P.number,
bio: P.optional(P.string),
posts: P.optional(P.array(postPattern)),
};
type User = P.infer<typeof userPattern>;
// User: { name: string, postCount: number, bio?: string, posts?: Post[] }
const isUserList = isMatching(P.array(userPattern));
const res = await fetchUsers();
if (isUserList(res)) {
// res: User
}
New type specific wildcards
P.symbol
P.symbol
is a wildcard pattern matching any symbol.
match(Symbol('Hello'))
.with(P.symbol, () => 'this is a symbol!')
.exhaustive();
P.bigint
P.bigint
is a wildcard pattern matching any bigint.
match(200n)
.with(P.bigint, () => 'this is a bigint!')
.exhaustive();
v3.3.5
v3.3.4
v3.3.2
This patch contains some compile time perf improvements.
@ahejlsberg recently implemented tail call elimination for recursive conditional types (microsoft/TypeScript#45711). This release is preparation work to take advantage of this new feature by making most type helper functions tail recursive. From the non scientific tests I made on my machine, this also improves the compilation time of the tests/
folder quite significantly on our current TS version (4.4). Compilation is ~ 20% faster.
v3.3.1
Features
Add a __.NaN
pattern, matching only NaN
s values. Thanks @mhintz for adding this
const res = match<number | null>(NaN)
.with(null, () => 'null!')
.with(__.NaN, () => 'NaN!')
.with(__.number, (x) => 'a number!')
.exhaustive();
console.log(res)
// => 'NaN!'
Bugfix
Update the __.number
pattern to also match on NaN
values.
Since NaN
has type number
in TypeScript, there is no way to distinguish a NaN
from a regular number at the type level. This was causing an issue where .exhaustive()
considered all numbers handled by the __.number
pattern even though NaN
wasn't matched by it, resulting in possible runtime errors.
const res = match<number | null>(NaN)
.with(null, () => 'null!')
.with(__.number, (x) => 'a number!')
// This used to throw at runtime because NaN wasn't matched by __.number
.exhaustive();
console.log(res)
// => 'a number!'