Skip to content

Releases: gvergnaud/ts-pattern

v4.3.0

08 May 22:31
f147f96
Compare
Choose a tag to compare

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

Full Changelog: v4.2.2...v4.3.0

v4.2.2

23 Mar 17:42
Compare
Choose a tag to compare

Bug fixes:

  • Issue #142: Fixes a type inference bug when the input type only has optional properties. commit 3c36992

v4.2.1

22 Feb 10:11
Compare
Choose a tag to compare

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

19 Feb 18:26
Compare
Choose a tag to compare

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

06 Feb 23:34
Compare
Choose a tag to compare

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

12 Jan 09:11
Compare
Choose a tag to compare

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 been updated to only rely on features of TS 4.5.

v4.1.2

08 Jan 21:25
Compare
Choose a tag to compare

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 neither import nor require.

v4.0.6

16 Nov 10:04
Compare
Choose a tag to compare

Bug fixes

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

16 Nov 09:58
Compare
Choose a tag to compare

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

06 Jul 13:33
Compare
Choose a tag to compare

Fixes:

  • 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 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 type unknown. This has been fixed.
const f = (x: unknown) => match(x).with([], () => "this is an empty array!").otherwise(() => "?")

Commits: