Skip to content

Commit 803554d

Browse files
authored
Merge pull request #139 from gvergnaud/gvergnaud/ts-5
TS-Pattern v5 base branch
2 parents f147f96 + ce1925d commit 803554d

60 files changed

Lines changed: 7291 additions & 4494 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 312 additions & 95 deletions
Large diffs are not rendered by default.

docs/roadmap.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
11
### Roadmap
22

3-
- [ ] Add a custom matcher protocol data structures could implement to make them matchable.
4-
- [ ] Add a native regex support.
5-
6-
- [ ] (Maybe) add an iterator protocol to `P.array` to be usable as a variadic tuple pattern. Example of using `P.array`:
3+
- [ ] chainable methods
4+
- [ ] string
5+
- [x] `P.string.includes('str')`
6+
- [x] `P.string.startsWith('str')`
7+
- [x] `P.string.endsWith('str')`
8+
- [ ] `P.string.regex('[a-z]+')`
9+
- [ ] numbers
10+
- [ ] `P.number.between(1, 10)`
11+
- [ ] `P.number.lt(12)`
12+
- [ ] `P.number.gt(12)`
13+
- [ ] `P.number.gte(12)`
14+
- [ ] `P.number.lte(12)`
15+
- [ ] `P.number.int(12)`
16+
- [ ] `P.number.finite`
17+
- [ ] `P.number.positive`
18+
- [ ] `P.number.negative`
19+
- [ ] all
20+
- [ ] `P.number.optional`
21+
- [ ] `P.string.optional`
22+
- [ ] `P.number.select()`
23+
- [ ] `P.string.select()`
24+
- [ ] `P.number.optional.select()`
25+
- [ ] `P.string.optional.select()`
26+
- [x] Add a custom matcher protocol data structures could implement to make them matchable.
27+
- [x] (Maybe) add an iterator protocol to `P.array` to be usable as a variadic tuple pattern. Example of using `P.array`:
728

829
```ts
930
const reverse = <T>(xs: T[]): T[] => {

docs/v4-to-v5-migration-guide.md

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
# TS-Pattern v4 to v5 Migration Guide
2+
3+
This file contains all breaking changes and new features between the version 4 and 5 of TS-Pattern.
4+
5+
# Breaking changes
6+
7+
## `.with` is now evaluated eagerly
8+
9+
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:
10+
11+
```ts
12+
// TS-Pattern v4
13+
type Input = { type: 'ok'; value: number } | { type: 'error'; error: Error };
14+
15+
// We don't call `.exhaustive`, so handlers don't run.
16+
function someFunction(input: Input) {
17+
match(input)
18+
.with({ type: 'ok' }, ({ value }) => {
19+
console.log(value);
20+
})
21+
.with({ type: 'error' }, ({ error }) => {
22+
throw error;
23+
});
24+
}
25+
26+
someFunction({ type: 'ok', value: 42 }); // nothing happens
27+
```
28+
29+
In **TS-Pattern v5**, however, the library will execute the matching handler as soon as it finds it:
30+
31+
```ts
32+
// TS-Pattern v5
33+
someFunction({ type: 'ok', value: 42 }); // logs "42" to the console!
34+
```
35+
36+
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`.
37+
38+
## Matching on Map and Sets
39+
40+
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:
41+
42+
```diff
43+
- import { match } from 'ts-pattern';
44+
+ import { match, P } from 'ts-pattern';
45+
46+
47+
const someFunction = (value: Set<number> | Map<string, number>) =>
48+
match(value)
49+
- .with(new Set([P.number]), (set) => `a set of numbers`)
50+
- .with(new Map([['key', P.number]]), (map) => `map.get('key') is a number`)
51+
+ .with(P.set(P.number), (set) => `a set of numbers`)
52+
+ .with(P.map('key', P.number), (map) => `map.get('key') is a number`)
53+
.otherwise(() => null);
54+
```
55+
56+
- The subpattern we provide in `P.set(subpattern)` should match all values in the set.
57+
- 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.
58+
59+
# New features
60+
61+
## chainable methods
62+
63+
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`.
64+
65+
Since a few examples is worth a thousand words, here are a few ways you can use chainable methods:
66+
67+
### P.number methods
68+
69+
```ts
70+
const example = (position: { x: number; y: number }) =>
71+
match(position)
72+
.with({ x: P.number.gte(100) }, (value) => '🎮')
73+
.with({ x: P.number.between(0, 100) }, (value) => '🎮')
74+
.with(
75+
{
76+
x: P.number.positive().int(),
77+
y: P.number.positive().int(),
78+
},
79+
(value) => '🎮'
80+
)
81+
.otherwise(() => 'x or y is negative');
82+
```
83+
84+
Here is the full list of number methods:
85+
86+
- `P.number.between(min, max)`: matches numbers between `min` and `max`.
87+
- `P.number.lt(max)`: matches numbers smaller than `max`.
88+
- `P.number.gt(min)`: matches numbers greater than `min`.
89+
- `P.number.lte(max)`: matches numbers smaller than or equal to `max`.
90+
- `P.number.gte(min)`: matches numbers greater than or equal to `min`.
91+
- `P.number.int()`: matches integers.
92+
- `P.number.finite()`: matches all numbers except `Infinity` and `-Infinity`
93+
- `P.number.positive()`: matches positive numbers.
94+
- `P.number.negative()`: matches negative numbers.
95+
96+
### P.string methods
97+
98+
```ts
99+
const example = (query: string) =>
100+
match(query)
101+
.with(P.string.startsWith('SELECT'), (query) => `selection`)
102+
.with(P.string.endsWith('FROM user'), (query) => `👯‍♂️`)
103+
.with(P.string.includes('*'), () => 'contains a star')
104+
// Methods can be chained:
105+
.with(P.string.startsWith('SET').includes('*'), (query) => `🤯`)
106+
.exhaustive();
107+
```
108+
109+
Here is the full list of string methods:
110+
111+
- `P.string.startsWith(str)`: matches strings that start with `str`.
112+
- `P.string.endsWith(str)`: matches strings that end with `str`.
113+
- `P.string.minLength(min)`: matches strings with at least `min` characters.
114+
- `P.string.maxLength(max)`: matches strings with at most `max` characters.
115+
- `P.string.includes(str)`: matches strings that contain `str`.
116+
- `P.string.regex(RegExp)`: matches strings if they match this regular expression.
117+
118+
### Global methods
119+
120+
Some methods are available for all primitive type patterns:
121+
122+
- `P.{..}.optional()`: matches even if this property isn't present on the input object.
123+
- `P.{..}.select()`: injects the matched value into the handler function.
124+
- `P.{..}.and(pattern)`: matches if the current pattern **and** the provided pattern match.
125+
- `P.{..}.or(pattern)`: matches if either the current pattern **or** the provided pattern match.
126+
127+
```ts
128+
const example = (value: unknown) =>
129+
match(value)
130+
.with(
131+
{
132+
username: P.string,
133+
displayName: P.string.optional(),
134+
},
135+
() => `{ username:string, displayName?: string }`
136+
)
137+
.with(
138+
{
139+
title: P.string,
140+
author: { username: P.string.select() },
141+
},
142+
(username) => `author.username is ${username}`
143+
)
144+
.with(
145+
P.instanceOf(Error).and({ source: P.string }),
146+
() => `Error & { source: string }`
147+
)
148+
.with(P.string.or(P.number), () => `string | number`)
149+
.otherwise(() => null);
150+
```
151+
152+
## Variadic tuple patterns
153+
154+
With TS-Pattern, you are now able to create array (or more accurately tuple) pattern with a variable number of elements:
155+
156+
```ts
157+
const example = (value: unknown) =>
158+
match(value)
159+
.with(
160+
// non-empty list of strings
161+
[P.string, ...P.array(P.string)],
162+
(value) => `value: [string, ...string[]]`
163+
)
164+
.otherwise(() => null);
165+
```
166+
167+
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:
168+
169+
```ts
170+
const example = (value: unknown) =>
171+
match(value)
172+
.with(
173+
[P.string, P.string, P.string, ...P.array(P.string)],
174+
(value) => `value: [string, string, string, ...string[]]`
175+
)
176+
.with(
177+
[P.string, P.string, ...P.array(P.string)],
178+
(value) => `value: [string, string, ...string[]]`
179+
)
180+
.with([], (value) => `value: []`)
181+
.otherwise(() => null);
182+
```
183+
184+
Fixed-index patterns can also be set **after** the `...P.array` variadic, or on both sides!
185+
186+
```ts
187+
const example = (value: unknown) =>
188+
match(value)
189+
.with(
190+
[...P.array(P.number), P.string, P.number],
191+
(value) => `value: [...number[], string, number]`
192+
)
193+
.with(
194+
[P.boolean, ...P.array(P.string), P.number, P.symbol],
195+
(value) => `value: [boolean, ...string[], number, symbol]`
196+
)
197+
.otherwise(() => null);
198+
```
199+
200+
Lastly, argument of `P.array` is now optional, and will default to `P._`, which matches anything:
201+
202+
```ts
203+
const example = (value: unknown) =>
204+
match(value)
205+
// 👇
206+
.with([P.string, ...P.array()], (value) => `value: [string, ...unknown[]]`)
207+
.otherwise(() => null);
208+
```
209+
210+
## `.returnType`
211+
212+
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`:
213+
214+
```ts
215+
// TS-Pattern v4
216+
match<
217+
{ isAdmin: boolean; plan: 'free' | 'paid' }, // input type
218+
number // return type
219+
>({ isAdmin, plan })
220+
.with({ isAdmin: true }, () => 123)
221+
.with({ plan: 'free' }, () => 'Oops!');
222+
// ~~~~~~ ❌ not a number.
223+
```
224+
225+
the main drawback is that you need to set the **_input type_** explicitly **_too_**, even though TypeScript should be able to infer it.
226+
227+
In TS-Pattern v5, you can use the `.returnType<Type>()` method to only set the return type:
228+
229+
```ts
230+
match({ isAdmin, plan })
231+
.returnType<number>() // 👈 new
232+
.with({ isAdmin: true }, () => 123)
233+
.with({ plan: 'free' }, () => 'Oops!');
234+
// ~~~~~~ ❌ not a number.
235+
```

examples/one-file-demo.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { isMatching, match, P } from 'ts-pattern';
2+
13
/**
24
* ### One file TS-Pattern demo.
35
*
@@ -11,8 +13,6 @@
1113
* `P.array`, `P.optional`, etc.
1214
*/
1315

14-
import { isMatching, match, P } from 'ts-pattern';
15-
1616
/**************************************************
1717
* Use case 1: handling discriminated union types *
1818
**************************************************/
@@ -22,7 +22,7 @@ type Response =
2222
| { type: 'image'; data: { extension: 'gif' | 'jpg' | 'png'; src: string } }
2323
| { type: 'text'; data: string; tags: { name: string; id: number }[] };
2424

25-
const exampleFunction1 = (input: Response): string =>
25+
const example1 = (input: Response): string =>
2626
match(input)
2727
// 1. Basic pattern with inference with a wildcard
2828
.with({ type: 'video', data: { format: 'mp4' } }, (video) => video.data.src)
@@ -57,7 +57,7 @@ type UserType = 'editor' | 'viewer';
5757
// Uncomment 'enterprise' to see exhaustive checking in action
5858
type OrgPlan = 'basic' | 'pro' | 'premium'; // | 'enterprise';
5959

60-
const exampleFunction2 = (org: OrgPlan, user: UserType) =>
60+
const example2 = (org: OrgPlan, user: UserType) =>
6161
// 1. Checking several enums with tuples
6262
match([org, user] as const)
6363
.with(['basic', P._], () => `Please upgrade to unlock this feature!`)
@@ -72,8 +72,31 @@ const exampleFunction2 = (org: OrgPlan, user: UserType) =>
7272
// 3. complex exhaustive checking
7373
.exhaustive();
7474

75+
/**************************************************
76+
* Use case 3: Matching specific strings or numbers
77+
**************************************************/
78+
79+
const example3 = (queries: string[]) =>
80+
match(queries)
81+
.with(
82+
[
83+
P.string.startsWith('SELECT').endsWith('FROM user').select(),
84+
...P.array(),
85+
],
86+
(firstQuery) => `${firstQuery}: 👨‍👩‍👧‍👦`
87+
)
88+
.with(P.array(), () => 'other queries')
89+
.exhaustive();
90+
91+
const example4 = (position: { x: number; y: number }) =>
92+
match(position)
93+
.with({ x: P.number.gte(100) }, (value) => '⏱️')
94+
.with({ x: P.number.between(0, 100) }, (value) => '⏱️')
95+
.with({ x: P.number.positive(), y: P.number.positive() }, (value) => '⏱️')
96+
.otherwise(() => 'x or y is negative');
97+
7598
/******************************************
76-
* Use case 3: Validation an API response *
99+
* Use case 4: Validation an API response *
77100
******************************************/
78101

79102
const userPattern = {
@@ -87,15 +110,17 @@ const userPattern = {
87110
};
88111

89112
const postPattern = {
90-
title: P.string,
113+
title: P.string.minLength(2).maxLength(255),
114+
stars: P.number.int().between(0, 5),
91115
content: P.string,
92-
likeCount: P.number,
93116
author: userPattern,
94117
// 2. arrays
95118
comments: P.array({
96119
author: userPattern,
97120
content: P.string,
98121
}),
122+
// 3. tuples (a non-empty array in this case)
123+
tags: [P.string, ...P.array(P.string)],
99124
};
100125

101126
type Post = P.infer<typeof postPattern>;

examples/package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
"main": "one-file-demo.ts",
55
"author": "gvergnaud",
66
"dependencies": {
7-
"ts-pattern": "^4.1.2"
7+
"ts-pattern": "^5.0.0-rc.1"
88
}
99
}

0 commit comments

Comments
 (0)