Skip to content

Releases: gvergnaud/ts-pattern

v2.2.3

08 Mar 18:44
Compare
Choose a tag to compare

A few bug fixes regarding type inference:

  • Improved support for readonly inputs
  • Fix a bug with exhaustive matching: string and number literals (like "hello" or 2) where inferred as string and number. When patterns were containing literals, the pattern matching expression was considered exhaustive, even though it wasn't.
  • Fix an inference issue with any: If an any type was contained in your input data structure, ts-pattern was sometimes unable to infer which branch of the input type was matching your pattern.

v2.2.2

03 Mar 08:47
Compare
Choose a tag to compare

Support for multiple patterns

You can now provide up to 5 patterns to .with(), and the code branch will be chosen if your input matches one of these patterns.

Example:

match<Country>('France')
      .exhaustive()
      // if it matches one of these patterns
      .with('France', 'Germany', 'Spain', () => 'Europe')
      .with('USA', () => 'America')
      .run();

    match<Country>('France')
      .exhaustive()
      .with('Germany', 'Spain', () => 'Europe')
      .with('USA', () => 'America')
      // doesn't compile! 'France' is missing
      .run();

v2.1.4

24 Feb 18:43
Compare
Choose a tag to compare

Bug Fixes:

  • The selection object you get in second parameter has a better type inference

General improvements:

  • Tests are now using Expect and Equal type utility functions

v2.1.3

23 Feb 08:17
Compare
Choose a tag to compare

This release features a major refactoring of the way exhaustive pattern matching is enforced, that should drastically improve its compile time performances on medium to large input types.

Motivation

.exhaustive() used to transform the input type in a flat union type, containing all the possible combination of all unions contained in the input type. For instance a type like:

type Input = {type: "a", mode: "b" | "c" | "d"} |  {type: "b", mode: "f" | "g"}

Would be turned into a type like this:

type DistributedInput =
  | {type: "a", mode: "b"} 
  | {type: "a", mode: "c"} 
  | {type: "a", mode: "d"} 
  | {type: "b", mode: "f"} 
  | {type: "b", mode: "g"}   

This solution was working fine, but the downside is that sometimes your Input type contains some huge unions that you never (or rarely) want to match against. Here is an example:

type CSSColor = "grey" | "red" | "yellow" | "blue" | "green" | ...; // hundreds of color names

type Input  =
  | { type: "text"; color: CSSColor }
  | { type: "button"; color: CSSColor; backgroundColor: CSSColor };

match(input)
    .exhaustive() // "union type that is too complex to represent"
    .with({ type: "button" }, () => ...)
    .with({ type: "text" }, () => ...)
    .run();

We hit the union limit of 500 items, even though we actually didn't need to transform the input type at all since we were only matching against its type property.

Changes

Now exhaustive matching is a lot smarter because it only computes the combination of unions against which you are really matching, which means that in the case described above the input type is unchanged:

match(input)
    .exhaustive() // nothing happens at this point, `Input` stay the same.
    .with({ type: "button" }, () => ...) // we match on the `type` property, we don't need to change `Input`.
    .with({ type: "text" }, () => ...) // same as above.
    .run(); // it works

If we were matching on a specific color, though, we would have to distribute the CSSColor union over the Input type:

match(input)
    .exhaustive() // input: Input
    .with({ type: "button" }, () => ...) // input: Input
    // below we are matching on both `type` and `color`, we need to 
    // distribute matched unions over the Input type:
    .with({ type: "text", color: "blue" }, () => ...) 
    /* input: | { type: "button"; color: CSSColor; backgroundColor: CSSColor }
     *        | { type: "text", color: "grey" }
     *        | { type: "text", color: "red" }
     *        | { type: "text", color: "yellow" }
     *        | ... all possible colors except "blue". We need to distribute at this point,
     *              but note that the `type: "button"` case hasn't been distributed, 
     *              so we don't reach the `union too complex to represent` limit.
     */     
    .run();

You can find more details in this issue #16 from @m-rutter

v2.1.3-next.0

22 Feb 10:06
Compare
Choose a tag to compare
v2.1.3-next.0 Pre-release
Pre-release

This pre-release features a major refactoring of the way exhaustive pattern matching is enforced, that should drastically improve its compile time performances on medium to large input types.

Motivation

.exhaustive() used to transform the input type in a flat union type, containing all the possible combination of all unions contained in the input type. For instance a type like:

type Input = {type: "a", mode: "b" | "c" | "d"} |  {type: "b", mode: "f" | "g"}

Would be turned into a type like this:

type DistributedInput =
  | {type: "a", mode: "b"} 
  | {type: "a", mode: "c"} 
  | {type: "a", mode: "d"} 
  | {type: "b", mode: "f"} 
  | {type: "b", mode: "g"}   

This solution was working fine, but the downside is that sometimes your Input type contains some huge unions that you never (or rarely) want to match against. Here is an example:

type CSSColor = "grey" | "red" | "yellow" | "blue" | "green" | ...; // hundreds of color names

type Input  =
  | { type: "text"; color: CSSColor }
  | { type: "button"; color: CSSColor; backgroundColor: CSSColor };

match(input)
    .exhaustive() // "union type that is too complex to represent"
    .with({ type: "button" }, () => ...)
    .with({ type: "text" }, () => ...)
    .run();

We hit the union limit of 500 items, even though we actually didn't need to transform the input type at all since we were only matching against its type property.

Changes

Now exhaustive matching is a lot smarter because it only computes the combination of unions against which you are really matching, which means that in the case described above the input type is unchanged:

match(input)
    .exhaustive() // nothing happens at this point, `Input` stay the same.
    .with({ type: "button" }, () => ...) // we match on the `type` property, we don't need to change `Input`.
    .with({ type: "text" }, () => ...) // same as above.
    .run(); // it works

If we were matching on a specific color, though, we would have to distribute the CSSColor union over the Input type:

match(input)
    .exhaustive() // input: Input
    .with({ type: "button" }, () => ...) // input: Input
    // below we are matching on both `type` and `color`, we need to 
    // distribute matched unions over the Input type:
    .with({ type: "text", color: "blue" }, () => ...) 
    /* input: | { type: "button"; color: CSSColor; backgroundColor: CSSColor }
     *        | { type: "text", color: "grey" }
     *        | { type: "text", color: "red" }
     *        | { type: "text", color: "yellow" }
     *        | ... all possible colors except "blue". We need to distribute at this point,
     *              but note that the `type: "button"` case hasn't been distributed, 
     *              so we don't reach the `union too complex to represent` limit.
     */     
    .run();

This change is pretty significant so please try it and tell me if it's working as expected!

You can find more details in this issue #16 from @m-rutter

v2.1.2

08 Feb 09:04
Compare
Choose a tag to compare
  • Improve support for generic types. Some type expressions containing generics did not reduce properly, even with simple catch all patterns. Now they do.
  • Improve compile time performance for exhaustive patterns. Exhaustive patterns used to take a lot of compilation time because DistributeUnions sometimes creates huge type expressions and we were computing the pattern type based on it. Now Pattern always takes the user defined Input type instead of the distributed input.

v2.1.1

04 Feb 08:57
ff74bca
Compare
Choose a tag to compare

This patch contains some small type checking perf improvements:

  • Only recurse on plain objects, not builtin ones like Error, Date, RegExp, Function, etc.
  • Stop recursion after 4 level of nesting in DistributeUnions to avoid unnecessarily traversing huge data structures looking for union types.

v2.1.0

30 Jan 09:46
Compare
Choose a tag to compare

This version introduces a new API to opt into exhaustive pattern matching, to let typescript make sure that all possible cases are handled and that your code won't throw at runtime.

Here is an example of this new API:

type Input = { kind: 'none' } | { kind: 'some'; value: number };

// This compiles:
match({ kind: 'some', value: 3 })
.exhaustive()
.with({ kind: 'some' }, ({ value }) => value)
.with({ kind: 'none' }, () => 0)
.run();

// This doesn't
match({ kind: 'some', value: 3 })
.exhaustive()
.with({ kind: 'some' }, ({ value }) => value)
.run();

This is a major version because it uses some features of the type system that were introduced in TS v4.x, like variadic tuples, and recursive conditional types.

Exhaustive pattern matching RC

17 Jan 22:04
Compare
Choose a tag to compare
Pre-release

This pre-release introduces a new API to opt into exhaustive checking, to let typescript make sure that all possible cases are handled and that your code won't throw at runtime.

Here is an example of this new API:

type Input = { kind: 'none' } | { kind: 'some'; value: number };

// This compiles:
match<Input>({ kind: 'some', value: 3 })
        .exhaustive()
        .with({ kind: 'some' }, ({ value }) => value)
        .with({ kind: 'none' }, () => 0)
        .run();

// This doesn't
match<Input>({ kind: 'some', value: 3 })
        .exhaustive()
        .with({ kind: 'some' }, ({ value }) => value)
        .run();

This is a major version because it uses some features of the type system that were introduced in TS v4.x, like variadic tuples, and recursive conditional types. People using earlier versions of TS will need to upgrade in order to use ts-pattern v2+.

v1.1.0

03 Jul 15:48
Compare
Choose a tag to compare
v1.1.0