Skip to content

Commit

Permalink
Merge pull request #139 from gvergnaud/gvergnaud/ts-5
Browse files Browse the repository at this point in the history
TS-Pattern v5 base branch
  • Loading branch information
gvergnaud committed Jun 13, 2023
2 parents f147f96 + ce1925d commit 803554d
Show file tree
Hide file tree
Showing 60 changed files with 7,291 additions and 4,494 deletions.
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"
}
}

0 comments on commit 803554d

Please sign in to comment.