Skip to content

指南 · 基础

版本基线 ts-pattern 5.9.0。本篇把「会写 match」用到「懂模式语言」:集合模式(P.array/P.set/P.record)、组合模式(P.union/P.not/P.optional)、守卫 P.when,以及类型收窄的基本机制。

速查

  • 基线:ts-pattern 5.9.0,零依赖、ESM / CommonJS 双入口、sideEffects: false
  • match(value) 自上而下短路,第一个命中的 .with() 胜出;具体模式应放在宽模式之前
  • 对象模式只要求列出的键匹配,输入可以含额外属性;命中后 handler 参数会按结构收窄
  • 集合:P.array(sub) / P.set(sub) 校验所有元素,P.record(key, value) 校验动态键值字典
  • 组合:P.union 是或、P.intersection 是与、P.not 是否定;组合越复杂越要保留明确兜底
  • P.optional(sub) 表示对象键可缺失;P.nullish 匹配值 null | undefined,两者不是同一语义
  • P.when(predicate) 是嵌入模式的守卫;.when(predicate, handler) 是 match 链上的独立分支
  • .exhaustive() 同时执行与检查穷尽性;开放类型无法列尽时用 .otherwise() 明确兜底

一、匹配是怎么进行的

match(value).with(...).with(...).exhaustive() 时:

  1. match(value) 拿到输入;
  2. 自上而下尝试每个 .with(pattern, handler)——第一个匹配成功的分支胜出(短路,后续不再尝试);
  3. 执行该分支的 handler,其返回值成为整条表达式的返回值;
  4. 末尾 .exhaustive() 在编译期检查:输入类型的所有可能是否都被覆盖。

关键:匹配短路 + 顺序敏感。把更具体的模式写在前、更宽泛的(P._、宽条件 P.when)写在后,否则宽模式会「截胡」具体分支。

二、对象模式与类型收窄

对象模式是结构性的:只校验你列出的键,输入可带额外属性。命中后 handler 参数被收窄:

ts
import { match, P } from "ts-pattern";

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rect"; width: number; height: number };

const area = (s: Shape) =>
  match(s)
    .with({ kind: "circle" }, (c) => Math.PI * c.radius ** 2) // c: circle 成员
    .with({ kind: "rect" }, (r) => r.width * r.height) // r: rect 成员
    .exhaustive();

在 circle 分支里访问 c.width 会编译报错——这正是类型收窄带来的安全:你只能访问当前分支确实拥有的字段。

三、集合模式

ts
import { match, P } from "ts-pattern";

match(input)
  // 数组:每个元素都满足子模式
  .with(P.array(P.number), (nums) => `数字数组(${nums.length})`)
  // 对象数组
  .with(P.array({ id: P.number }), (rows) => `共 ${rows.length} 行`)
  // 字典:所有键为 string、值为 number
  .with(P.record(P.string, P.number), (scores) => "分数表")
  // Set
  .with(P.set(P.string), () => "字符串集合")
  .otherwise(() => "其它");
  • P.array(sub) 逐元素校验子模式,不限长度(含空数组);省略 sub 则不约束元素。
  • P.record(keySub, valSub) 面向「动态键的字典」,区别于「固定字段的对象模式」。

四、组合模式:union / not / intersection

ts
import { match, P } from "ts-pattern";

match(input)
  // 或:命中任一即可
  .with({ type: P.union("user", "org") }, (x) => x.name)
  // 否定:非布尔值
  .with(P.not(P.boolean), (v) => v)
  // 与:既是 A 实例又有 foo === 'bar'
  .with(P.intersection(P.instanceOf(A), { foo: "bar" }), (a) => a.foo)
  .otherwise(() => null);
  • P.union(...) 是逻辑或,子模式可为字面量或其它 P.*
  • P.intersection(...) 是逻辑与,命中后类型取交集,常用于「类身份 + 额外结构约束」。
  • P.not(sub) 匹配「不满足 sub」的值,配合 exhaustive 能优雅地「先排除一类,再处理其余」。

五、可选属性:P.optional

P.optional 只在对象属性位置有意义——标记某键可缺失,存在则须满足子模式:

ts
import { match, P } from "ts-pattern";

const f = (input: { key?: string }) =>
  match(input)
    .with({ key: P.optional(P.string) }, (a) => a.key) // a.key: string | undefined
    .exhaustive();

P.optional ≠ P.nullish

P.optional 关注「键是否存在」(缺失也命中);P.nullish 关注「值是否为 null/undefined」。二者语义不同,别混用。

六、守卫:P.when

结构和字面量表达不了的条件(如「大于 5」「长度为偶数」),用 P.when(predicate)

ts
import { match, P } from "ts-pattern";

const emoji = (input: { score: number }) =>
  match(input)
    .with({ score: P.when((n) => n >= 90) }, () => "🏆")
    .with({ score: P.when((n) => n < 60) }, () => "😞")
    .otherwise(() => "🙂");

谓词返回 true 才命中。若写成类型守卫(返回 x is T),命中分支里还会进一步收窄类型:

ts
.with({ score: P.when((n): n is 100 => n === 100) }, () => '满分') // n 收窄为字面量 100

区分两个 when:P.when嵌在模式内部的模式;.when(predicate, handler)match 链上的方法,对整个输入加条件作为独立分支。


进入 指南 · 进阶P.select 数据提取、带守卫的 .with、判别联合与状态机、isMatching 类型守卫、变长元组。