Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TS-Pattern v5 base branch #139

Merged
merged 88 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
de5304a
v5: use as const for type parameters
gvergnaud Jan 21, 2023
ba63947
type internals as any
gvergnaud Jan 22, 2023
8c46181
types: merge unions of objects for pattern upperbound
gvergnaud Jan 22, 2023
da29f2f
types: add a const overload to match
gvergnaud Jan 22, 2023
c03a0a6
types: remove unnecessary as const
gvergnaud Jan 22, 2023
9518ced
API: make map and set Matcher functions instead of custom patterns
gvergnaud Jan 22, 2023
2d9fd35
dep: update typescript
gvergnaud Feb 8, 2023
4e4d627
make pattern matching strict
gvergnaud Feb 8, 2023
2070c7e
tspattern 5 variadic tuple support
gvergnaud Feb 10, 2023
ce50668
variadic: support variadic in extract precise value
gvergnaud Feb 11, 2023
9a726a2
variadic: support property selection
gvergnaud Feb 11, 2023
e59afc6
variadic: rename variable in isMatching
gvergnaud Feb 11, 2023
83a0b94
variadic: support is matching
gvergnaud Feb 11, 2023
60928fc
deps: update prettier
gvergnaud Feb 11, 2023
a7f1714
variadic: support exhaustive matching 🎉
gvergnaud Feb 11, 2023
75c15b1
variadic: fix unit test
gvergnaud Feb 11, 2023
17d16b5
set & map: allow no arguments
gvergnaud Feb 11, 2023
d35b92c
variadic: add runtime implementation
gvergnaud Feb 11, 2023
0b8b3cf
returnType: add feature
gvergnaud Feb 11, 2023
66dd400
rename variables
gvergnaud Feb 12, 2023
be3ea94
fix: don't use const type param in when because it's incompatible wit…
gvergnaud Feb 19, 2023
415b980
variadic tuples: add tests
gvergnaud Feb 20, 2023
723fc45
v5: better support for wrong patterns resulting in the 'any' type
gvergnaud Apr 8, 2023
5b7041e
deps: update prettier
gvergnaud Apr 29, 2023
2b7f2d9
deps: update package.lock
gvergnaud May 7, 2023
056f439
feat: Add custom matcher protocol
gvergnaud Jan 21, 2023
ce9fd4a
reposition comment
gvergnaud May 7, 2023
4c7d11b
chainable: setup test file
gvergnaud May 8, 2023
fa84c1c
matcher: simplify function protocol
gvergnaud May 8, 2023
d364fdb
matcher: simplify matcher protocol and mark unstable
gvergnaud May 9, 2023
a78021a
chainable: add a few string chainable methods
gvergnaud May 9, 2023
67be1e1
chainable: Add number functions
gvergnaud May 14, 2023
32d8430
chainable: Add bigint functions
gvergnaud May 14, 2023
36b2b5c
chainable: Add base chainable methods
gvergnaud May 14, 2023
9d72d6b
chainable: Allow chaining type specific methods
gvergnaud May 14, 2023
ee4f0f1
chainable: Make when not chainable
gvergnaud May 14, 2023
19bba56
chainable: Add P.shape, and add pattern aliases
gvergnaud May 14, 2023
146e7bd
chainable: Fix select with key
gvergnaud May 15, 2023
d68fbaf
5.0.0
gvergnaud May 15, 2023
69b021f
5.0.0-rc.0
gvergnaud May 15, 2023
24faaef
chainable: add minLength and maxLength
gvergnaud May 15, 2023
c9c1e5c
invert pattern: fix object inference when input is unknown
gvergnaud May 15, 2023
de9ed9f
is matching: replace input type by unknown
gvergnaud May 16, 2023
373cefb
5.0.0-rc.1
gvergnaud May 16, 2023
4856172
example: update one-file-demo
gvergnaud May 20, 2023
5f762f7
v5: add tests
gvergnaud May 20, 2023
7c4d9df
v5: add migration guide
gvergnaud Jun 2, 2023
b204aed
v5: update migration guide
gvergnaud Jun 3, 2023
9c1689c
v5: Add subpattern and variadic tuple doc to readme
gvergnaud Jun 3, 2023
7d7cabd
v5: Readme
gvergnaud Jun 4, 2023
27dbee0
v5: cleanup unused imports
gvergnaud Jun 4, 2023
bea94d7
v5: update deps
gvergnaud Jun 4, 2023
136ef31
v5: make all equality checks const
gvergnaud Jun 4, 2023
5f1c144
5.0.0-rc.2
gvergnaud Jun 4, 2023
bdd68ed
v5: readme
gvergnaud Jun 4, 2023
7146461
v5: Fix pattern matching on generics
gvergnaud Jun 4, 2023
96922f8
5.0.0-rc.3
gvergnaud Jun 4, 2023
0b91ce0
v5: improve readme
gvergnaud Jun 5, 2023
efa2ced
v5: fix narrowing of optional properties in deeply nested arrays
gvergnaud Jun 5, 2023
661387e
5.0.0-rc.4
gvergnaud Jun 5, 2023
d0a515e
build: target es2017 for esm
gvergnaud Jun 5, 2023
5750e10
5.0.0-rc.5
gvergnaud Jun 5, 2023
d999b25
v5: update doc comments
gvergnaud Jun 8, 2023
6bda633
v5: fix issue #153
gvergnaud Jun 8, 2023
96f2fb7
v5: Remove undocumented P.typed function
gvergnaud Jun 8, 2023
6379df4
v5: comments
gvergnaud Jun 8, 2023
eaf8dd0
v5: refactor Pattern type
gvergnaud Jun 8, 2023
60ade5c
v5: Improve Pattern displayed type
gvergnaud Jun 8, 2023
d335b8a
v5: Make sure exhaustive error are displayed correctly
gvergnaud Jun 8, 2023
aaa3e08
v5: Add type error unit test
gvergnaud Jun 8, 2023
b6966e5
5.0.0-rc.6
gvergnaud Jun 9, 2023
338e05d
v5: Remove unused symbols
gvergnaud Jun 9, 2023
58528e4
v5: Simplify internal types of MatchExpression
gvergnaud Jun 9, 2023
70f02d6
v5: remove unused import
gvergnaud Jun 9, 2023
4366ab0
v5: Make MatchState objects homomorphic
gvergnaud Jun 9, 2023
03dbfe6
perf(v5): reuse unmatched objects between branches
gvergnaud Jun 9, 2023
afbc44f
v5: update isMatching to take a value of type unknown instead of any
gvergnaud Jun 9, 2023
5c4c03f
v5: only call otherwise with a single argument
gvergnaud Jun 9, 2023
8fe3e12
v5: fix typo in thrown error message
gvergnaud Jun 10, 2023
eec53f6
v5: simplify type
gvergnaud Jun 12, 2023
a0415cf
5.0.0-rc.7
gvergnaud Jun 12, 2023
27484fb
v5: improve doc comments
gvergnaud Jun 12, 2023
b48c486
returnType: add unit test
gvergnaud Jun 12, 2023
72d4cab
variadic: add support for selecting variadic part
gvergnaud Jun 13, 2023
29a2a43
5.0.0-rc.8
gvergnaud Jun 13, 2023
862d1dd
shape: mark pattern as a const type parameter
gvergnaud Jun 13, 2023
d3a6a12
readme: update pkg size
gvergnaud Jun 13, 2023
ce1925d
5.0.0
gvergnaud Jun 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
407 changes: 312 additions & 95 deletions README.md

Large diffs are not rendered by default.

29 changes: 25 additions & 4 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
### Roadmap

- [ ] Add a custom matcher protocol data structures could implement to make them matchable.
- [ ] Add a native regex support.

- [ ] (Maybe) add an iterator protocol to `P.array` to be usable as a variadic tuple pattern. Example of using `P.array`:
- [ ] chainable methods
- [ ] string
- [x] `P.string.includes('str')`
- [x] `P.string.startsWith('str')`
- [x] `P.string.endsWith('str')`
- [ ] `P.string.regex('[a-z]+')`
- [ ] numbers
- [ ] `P.number.between(1, 10)`
- [ ] `P.number.lt(12)`
- [ ] `P.number.gt(12)`
- [ ] `P.number.gte(12)`
- [ ] `P.number.lte(12)`
- [ ] `P.number.int(12)`
- [ ] `P.number.finite`
- [ ] `P.number.positive`
- [ ] `P.number.negative`
- [ ] all
- [ ] `P.number.optional`
- [ ] `P.string.optional`
- [ ] `P.number.select()`
- [ ] `P.string.select()`
- [ ] `P.number.optional.select()`
- [ ] `P.string.optional.select()`
- [x] Add a custom matcher protocol data structures could implement to make them matchable.
- [x] (Maybe) add an iterator protocol to `P.array` to be usable as a variadic tuple pattern. Example of using `P.array`:

```ts
const reverse = <T>(xs: T[]): T[] => {
Expand Down
235 changes: 235 additions & 0 deletions docs/v4-to-v5-migration-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# TS-Pattern v4 to v5 Migration Guide

This file contains all breaking changes and new features between the version 4 and 5 of TS-Pattern.

# 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
// 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
// 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 Map 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:

```diff
- 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

```ts
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

```ts
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.

```ts
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:

```ts
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:

```ts
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!

```ts
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:

```ts
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
// 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:

```ts
match({ isAdmin, plan })
.returnType<number>() // 👈 new
.with({ isAdmin: true }, () => 123)
.with({ plan: 'free' }, () => 'Oops!');
// ~~~~~~ ❌ not a number.
```
39 changes: 32 additions & 7 deletions examples/one-file-demo.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isMatching, match, P } from 'ts-pattern';

/**
* ### One file TS-Pattern demo.
*
Expand All @@ -11,8 +13,6 @@
* `P.array`, `P.optional`, etc.
*/

import { isMatching, match, P } from 'ts-pattern';

/**************************************************
* Use case 1: handling discriminated union types *
**************************************************/
Expand All @@ -22,7 +22,7 @@ type Response =
| { type: 'image'; data: { extension: 'gif' | 'jpg' | 'png'; src: string } }
| { type: 'text'; data: string; tags: { name: string; id: number }[] };

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

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

/**************************************************
* Use case 3: Matching specific strings or numbers
**************************************************/

const example3 = (queries: string[]) =>
match(queries)
.with(
[
P.string.startsWith('SELECT').endsWith('FROM user').select(),
...P.array(),
],
(firstQuery) => `${firstQuery}: 👨‍👩‍👧‍👦`
)
.with(P.array(), () => 'other queries')
.exhaustive();

const example4 = (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(), y: P.number.positive() }, (value) => '⏱️')
.otherwise(() => 'x or y is negative');

/******************************************
* Use case 3: Validation an API response *
* Use case 4: Validation an API response *
******************************************/

const userPattern = {
Expand All @@ -87,15 +110,17 @@ const userPattern = {
};

const postPattern = {
title: P.string,
title: P.string.minLength(2).maxLength(255),
stars: P.number.int().between(0, 5),
content: P.string,
likeCount: P.number,
author: userPattern,
// 2. arrays
comments: P.array({
author: userPattern,
content: P.string,
}),
// 3. tuples (a non-empty array in this case)
tags: [P.string, ...P.array(P.string)],
};

type Post = P.infer<typeof postPattern>;
Expand Down
14 changes: 7 additions & 7 deletions examples/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"main": "one-file-demo.ts",
"author": "gvergnaud",
"dependencies": {
"ts-pattern": "^4.1.2"
"ts-pattern": "^5.0.0-rc.1"
}
}
Loading