ts-pattern

🎨 The exhaustive Pattern Matching library for TypeScript, with smart type inference.

MIT License

Downloads
5.9M
Stars
10.9K
Committers
14

Bot releases are hidden (Show)

ts-pattern - v5.1.1 Latest Release

Published by gvergnaud 6 months ago

What's Changed

Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.1.0...v5.1.1

ts-pattern - v5.1.0

Published by gvergnaud 7 months ago

New features

P.nonNullable wildcard

Add 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.

What's Changed

Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.0.8...v5.1.0

ts-pattern - v5.0.8

Published by gvergnaud 8 months ago

The main thing

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.

What's Changed

Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.0.6...v5.0.8

ts-pattern - v5.0.6

Published by gvergnaud 11 months ago

Close issue issues

What's Changed

New Contributors

Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.0.5...v5.0.6

ts-pattern - v5.0.5

Published by gvergnaud about 1 year ago

Bug fixes

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
ts-pattern - v5.0.4

Published by gvergnaud about 1 year ago

What's Changed

Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.0.3...v5.0.4

ts-pattern - v5.0.3

Published by gvergnaud over 1 year ago

What's Changed

Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.0.2...v5.0.3

ts-pattern - v5.0.2

Published by gvergnaud over 1 year ago

What's Changed

New Contributors

Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v5.0.0...v5.0.2

ts-pattern - v5.0.0 ❤️

Published by gvergnaud over 1 year ago

TS-Pattern v5 is finally out ❤️

Breaking changes

.with is now evaluated eagerly

In 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 on Maps and Sets

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);
  • The subpattern we provide in P.set(subpattern) should match all values in the set.
  • The value subpattern we provide in P.map(keyPattern, subpattern) should only match the values matching keyPattern for the whole P.map(..) pattern to match the input.

New features

chainable methods

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:

P.number 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 -Infinity
  • P.number.positive(): matches positive numbers.
  • P.number.negative(): matches negative numbers.

P.string methods

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.

Global methods

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);

Variadic tuple patterns

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);

.returnType

In 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.

What's Changed

Full Changelog: https://github.com/gvergnaud/ts-pattern/compare/v4.3.0...v5.0.0

ts-pattern - v4.3.0

Published by gvergnaud over 1 year ago

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: https://github.com/gvergnaud/ts-pattern/compare/v4.2.2...v4.3.0

ts-pattern - v4.2.2

Published by gvergnaud over 1 year ago

Bug fixes:

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

Published by gvergnaud over 1 year ago

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');
ts-pattern - v4.2.0

Published by gvergnaud over 1 year ago

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: https://github.com/gvergnaud/ts-pattern/issues/140

ts-pattern - v4.1.4

Published by gvergnaud over 1 year ago

Bug fixes

Issue https://github.com/gvergnaud/ts-pattern/issues/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 (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(); // ✅
ts-pattern - v4.1.3

Published by gvergnaud almost 2 years ago

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.
ts-pattern - v4.1.2

Published by gvergnaud almost 2 years ago

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.
ts-pattern - v4.0.6

Published by gvergnaud almost 2 years ago

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()
ts-pattern - v4.0.5

Published by gvergnaud almost 2 years ago

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)

ts-pattern - v4.0.4

Published by gvergnaud over 2 years ago

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:

ts-pattern - v4.0.2

Published by gvergnaud over 2 years ago

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.