ts-pattern
GitHub - gvergnaud/ts-pattern: 🎨 The exhaustive Pattern Matching library for TypeScript, with smart
模式匹配
模式匹配语法(Pattern Matching Syntax)是一种用于检查数据结构和分支处理的编程技术,允许程序根据特定的模式匹配来选择不同的逻辑分支。它在许多编程语言中有所应用,例如在函数式编程语言(如Haskell、Scala)和最新版本的Python中。
Rust 中的模式匹配例子:
Rust
fn main() {
let x = 3;
match x {
1 => println!("One"),
2 => println!("Two"),
3 => println!("Three"),
_ => println!("Something else"),
}
}
和 if/switch 的对比
-
模式匹配可以由编译器检查是否覆盖了所有情形
-
模式匹配可以有返回值
-
可以匹配对象/数组或其他高级数据结构,支持传入函数提供复杂逻辑匹配
-
某些情况下,pattern match 会简洁优雅非常多
JavaScript 中的模式匹配
TypeScript
match (res) {
when { status: 200, let body, ...let rest }: handleData(body, rest);
when { const status, destination: let url } and if (300 <= status && status < 400):
handleRedirect(url);
when { status: 500 } and if (!this.hasRetried): do {
retry(req);
this.hasRetried = true;
};
default: throwSomething();
}
JavaScript 中的模式匹配语法早有提案(2018年已进入 Stage 1),但直到 2024 年都未有实际进展,甚至连 Primitive Patterns 都没有给出具体 Example,想在生产中用上该语法不知道要猴年马月,但是没关系,社区已经为 JavaScript/TypeScript 带来了模式匹配,包括但不限于:
-
Optionals --- Rust-like error handling, options and exhaustive pattern matching for TypeScript and Deno
-
ts-pattern --- Exhaustive Pattern Matching library for TypeScript, with smart type inference.
-
babel-plugin-proposal-pattern-matching --- Minimal grammar, high performance JavaScript pattern matching implementation.
-
match-iz --- A tiny functional pattern-matching library inspired by the TC39 proposal.
-
patcom --- Feature parity with TC39 proposal without any new syntax
本文重点讲解 ts-pattern
用法
如果只是为 JavaScript 增加模式匹配功能,那么这个库可能就叫做 js-pattern 或者 es-pattern 了,既然叫做 ts-pattern,那么说明这个库一定和 TypeScript 做了深层次绑定,下面开始介绍。
基础用法示例
TypeScript
import { match, P } from 'ts-pattern';
type Data =
| { type: 'text'; content: string }
| { type: 'img'; src: string };
type Result =
| { type: 'ok'; data: Data }
| { type: 'error'; error: Error };
const result: Result = ...;
const html = match(result)
.with({ type: 'error' }, () => <p>Oups! An error occured</p>)
.with({ type: 'ok', data: { type: 'text' } }, (res) => <p>{res.data.content}</p>)
.with({ type: 'ok', data: { type: 'img', src: P.select() } }, (src) => <img src={src} />)
.exhaustive(); // 运行 match 表达式并返回结果,同时保证在编译时已经处理了所有可能的情况
match
用来构造一个模式匹配表达式
TypeScript
match(value);
with
传入 pattern 并执行相对应的 handler
TypeScript
match(...)
.with(pattern, [...patterns], handler)
匹配原始值
TypeScript
import { match } from 'ts-pattern'
const val = 0 as 0 | 1
const result = match(val)
.with(0, () => '0')
.with(1, () => '1')
.exhaustive()
console.log(result)
匹配对象
TypeScript
import { match } from 'ts-pattern'
const val = { status: 'success' } as {
status: 'success' | 'failed' | 'loading'
}
const result = match(val)
.with({ status: 'loading' }, () => '加载中')
.with({ status: 'success' }, () => '成功啦')
.with({ status: 'failed' }, () => '失败了')
.exhaustive()
console.log(result)
匹配类型
TypeScript
import { match, P } from 'ts-pattern'
const val = 123 as number | string
const result = match(val)
.with(P.number, () => '我是数字')
.with(P.string, () => '我是字符串')
.exhaustive()
console.log(result)
匹配数组
TypeScript
import { match, P } from 'ts-pattern';
type Input = { title: string; content: string }[];
let input: Input = [
{ title: 'Hello world!', content: 'This is a very interesting content' },
{ title: 'Bonjour!', content: 'This is a very interesting content too' },
];
const output = match(input)
.with(
P.array({ title: P.string, content: P.string }),
(posts) => 'a list of posts!'
)
.otherwise(() => 'something else');
console.log(output);
// => 'a list of posts!'
when
当 ts-pattern 内置的 pattern 规则不满足你的需求时,可以使用 when 来自定义匹配规则
TypeScript
match(...)
.when(predicate, handler)
TypeScript
import { match, P } from 'ts-pattern'
const val = 10 as number
const result = match(val)
.when(
(num) => num > 5,
() => '大于5耶'
)
.when(
(num) => num < 5,
() => '小于5耶'
)
.otherwise(() => '其他情况')
console.log(result)
为了保证 when 函数能够正确推断,应该尽可能保证传入的 predicate 可以做到类型收窄。
以 string | number 举例
TypeScript
import { match, P } from 'ts-pattern'
const val = 10 as number | string
function isNumber(x): x is number {
return typeof x === 'number'
}
function isString(x): x is string {
return typeof x === 'string'
}
const result = match(val)
.when(isNumber, () => '是number')
.when(isString, () => '是string')
.exhaustive()
console.log(result)
由于 isNumber 和 isString 都可以正确的收窄类型,所以可以调用 exhaustive,但是如果改成:
TypeScript
import { match, P } from 'ts-pattern'
const val = 10 as number | string
function isNumber(x): boolean {
return typeof x === 'number'
}
function isString(x): boolean {
return typeof x === 'string'
}
const result = match(val)
.when(isNumber, () => '是number')
.when(isString, () => '是string')
.exhaustive()
console.log(result)
将会抛出一个 TypeScript 编译时错误:
TypeScript
This expression is not callable.
Type 'NonExhaustiveError<string | number>' has no call signatures.ts(2349)
exhaustive
TypeScript Never
熟悉 TypeScript Never 的同学可能知道,never 有一个很有用的用法就是保证所有的可能都能被穷举,我们来看一个具体的例子:
TypeScript
type All = 0 | 1
function handleVal(val: All) {
switch (val) {
case 0: {
let _test = val;
// 这里 val 被收窄类型至 0
break;
}
case 1: {
let _test = val;
// 这里 val 被收窄类型至 1
break;
}
default: {
// 这里 val 被收窄类型至 never
const _exhaustiveCheck: never = val
break;
}
}
}
由于 TypeScript 有类型收窄能力,在上面的 switch 语句中,default 块作用域中 val 一定会被收窄至 never 类型,因为我们已经在前面的 case 中穷举了所有类型,此时定义一个类型为 never 的 _exhaustiveCheck 变量,并且把 val 赋值给 _exhaustiveCheck,是可以赋值成功的。
修改上述 All 类型代码
TypeScript
type All = 0 | 1 | 2 // 加了个 2
由于 TypeScript 的类型收窄,default 块中的 val 变量会被收窄至 2 类型,此时赋值语句将会报错
Type 'number' is not assignable to type 'never'.(2322)
也就是说,我们可以利用这个小技巧保证在编译时一定能处理所有可能的情况,将运行时错误提前暴露至编译时。
**exhaustive:adj.**详尽的;彻底的;全面的
exhaustive 就是 ts-pattern 出的用来确保一定能进行详尽匹配的函数,还是以上述的 0 | 1 | 2 来举例。
TypeScript
import { match } from 'ts-pattern'
type Val = 0 | 1 | 2
const val = 0 as Val
match(val)
.with(0, () => console.log('是0'))
.with(1, () => console.log('是1'))
.with(2, () => console.log('是2'))
.exhaustive()
如果没有写 .with(2, () => console.log('是2')) 这一行,直接调用 exhaustive 会发生:
虽然代码在运行时可以正确执行,但是会有编译时错误。
otherwise
对于不可能在编译时穷举完的类型,调用 exhaustive 一定会报错,比如 number 类型,此时可以使用 otherwise来对其他情况做兜底,类似 switch 中的 default。
TypeScript
import { match } from 'ts-pattern'
const val = 123123 as number
match(val)
.with(0, () => console.log('是0'))
.with(1, () => console.log('是1'))
.otherwise(() => console.log('原来是其他数字啊'))
run
run 运行 match 表达式,但由于没有兜底代码,会直接在运行时进行报错,不建议使用
TypeScript
import { match } from 'ts-pattern'
const val = 123123 as number
match(val)
.with(0, () => console.log('是0'))
.with(1, () => console.log('是1'))
.run()
抛出运行时错误:error: Pattern matching error: no pattern matches value 123123
returnType
returnType 没有任何运行时作用,仅在编译时起作用,用于限制该 match 表达式的返回类型
TypeScript
returnType() {
return this;
}
TypeScript
import { match } from 'ts-pattern'
const val = 123123 as number
match(val)
.returnType<string>()
.with(0, () => '0')
.with(1, () => '1')
.with(2, () => 2)
.run()
其他高级用法
其他用法请自行查阅 ts-pattern 文档~
常用 Case
不同值展示不同内容
TypeScript
import { useState } from 'react'
import { match } from 'ts-pattern'
function Test() {
const [currentTab, setCurrentTab] = useState<0 | 1 | 2>(0)
return (
<Tabs>
{match(currentTab)
.with(0, () => <Tabs.Tab>Tab 0</Tabs.Tab>)
.with(1, () => <Tabs.Tab>Tab 1</Tabs.Tab>)
.with(2, () => <Tabs.Tab>Tab 2</Tabs.Tab>)
.exhaustive()}
</Tabs>
)
}
复杂对象条件分支众多
TypeScript
import { match } from 'ts-pattern'
type Result = {
a: 'A1' | 'A2'
b: 'B1' | 'B2' | 'B3'
}
const val = { a: 'A1', b: 'B2' } as Result
const result = match(val)
.with({ a: 'A1', b: 'B1' }, () => 'A1' + 'B1')
.with({ a: 'A1', b: 'B2' }, () => 'A1' + 'B2')
.with({ a: 'A1', b: 'B3' }, () => 'A1' + 'B3')
.with({ a: 'A2', b: 'B1' }, () => 'A2' + 'B1')
.with({ a: 'A2', b: 'B2' }, () => 'A2' + 'B2')
.with({ a: 'A2', b: 'B3' }, () => 'A2' + 'B3')
.exhaustive()
console.log(result)