ts-pattern - TypeScript 的详尽模式匹配库

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 中的模式匹配

github.com/tc39/propos...

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)
相关推荐
哑巴语天雨6 小时前
React+Vite项目框架
前端·react.js·前端框架
初遇你时动了情7 小时前
react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
前端·javascript·react.js
码农老起7 小时前
掌握 React:组件化开发与性能优化的实战指南
react.js·前端框架
前端没钱8 小时前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js
高山我梦口香糖11 小时前
[react] <NavLink>自带激活属性
前端·javascript·react.js
撸码到无法自拔11 小时前
React:组件、状态与事件处理的完整指南
前端·javascript·react.js·前端框架·ecmascript
高山我梦口香糖11 小时前
[react]不能将类型“string | undefined”分配给类型“To”。 不能将类型“undefined”分配给类型“To”
前端·javascript·react.js
乐闻x13 小时前
VSCode 插件开发实战(四):使用 React 实现自定义页面
ide·vscode·react.js
irisMoon0613 小时前
react项目框架了解
前端·javascript·react.js
web150850966411 天前
【React&前端】大屏适配解决方案&从框架结构到实现(超详细)(附代码)
前端·react.js·前端框架