|
| 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 | +``` |
0 commit comments