Releases: gvergnaud/ts-pattern
v2.1.3
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();
v2.1.3-next.0
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!
v2.1.2
- 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
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
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
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+.