Releases: gvergnaud/ts-pattern
v4.3.0
TS-Pattern and node16
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 ❤️
What's Changed
- build: better support moduleResolution: bundler by @gvergnaud in #158
- Gvergnaud/fix build for nodenext and commonjs by @gvergnaud in #160
Full Changelog: v4.2.2...v4.3.0
v4.2.2
v4.2.1
Bug fixes
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');
v4.2.0
Features
Better inference for match
and .with
match
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 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: #140
v4.1.4
Bug fixes
Issue #138 — inference issues with P.not
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
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.
Caveats
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 (microsoft/TypeScript#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(); // ✅
v4.1.3
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: