已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

模式匹配

使用 Match 模块进行模式匹配,简化复杂的分支逻辑。

模式匹配是一种让开发者能够在单个简洁表达式中处理复杂条件的方法。它简化了代码,使其更简洁、更容易理解。此外,它还包含一个称为穷尽性检查(exhaustiveness checking)的过程,用于帮助确保没有任何可能的情况被遗漏。

模式匹配源自函数式编程语言,是代码分支处理的一项强大技术。与 if/else 或 switch 语句这类命令式替代方案相比,它通常能提供更强大、更简洁的解决方案,尤其是在处理复杂条件时。

尽管模式匹配还不是 JavaScript 的原生特性,但目前有一个处于早期阶段的 tc39 提案,旨在把模式匹配引入 JavaScript。不过,该提案仍处于第 1 阶段,可能还需要数年才能落地。即便如此,开发者依然可以在自己的代码库中实现模式匹配。effect/Match 模块提供了一套可靠且类型安全的模式匹配实现,可立即使用。

示例(用模式匹配处理不同的数据类型)

import { Match } from "effect"

// Simulated dynamic input that can be a string or a number
const input: string | number = "some input"

//      ┌─── string
//      ▼
const result = Match.value(input).pipe(
  // Match if the value is a number
  Match.when(Match.number, (n) => `number: ${n}`),
  // Match if the value is a string
  Match.when(Match.string, (s) => `string: ${s}`),
  // Ensure all possible cases are covered
  Match.exhaustive,
)

console.log(result)
result // => "string: some input"

模式匹配的工作原理

模式匹配遵循一个结构化的流程:

  1. 创建匹配器。 定义一个 Matcher,让它作用于某个特定的类型

  2. 定义模式。 使用 Match.whenMatch.notMatch.tag 这类组合子来指定匹配条件。

  3. 完成匹配。 应用 Match.exhaustiveMatch.orElseMatch.option 这样的终结器,来决定未匹配的情况应如何处理。

创建匹配器

你可以通过以下任意一种方式创建 Matcher

  • Match.type<T>():针对特定的类型进行匹配。
  • Match.value(value):针对特定的值进行匹配。

按类型匹配

Match.type 构造函数会定义一个作用于特定类型的 Matcher。创建之后,你就可以使用 Match.when 这类模式来定义处理不同情况的条件。

示例(匹配数字和字符串)

import { Match } from "effect"

// Create a matcher for values that are either strings or numbers
//
//      ┌─── (u: string | number) => string
//      ▼
const match = Match.type<string | number>().pipe(
  // Match when the value is a number
  Match.when(Match.number, (n) => `number: ${n}`),
  // Match when the value is a string
  Match.when(Match.string, (s) => `string: ${s}`),
  // Ensure all possible cases are handled
  Match.exhaustive,
)

console.log(match(0))
match(0) // => "number: 0"

console.log(match("hello"))
match("hello") // => "string: hello"

按值匹配

除了为类型创建匹配器,你也可以使用 Match.value 直接基于某个具体的值来定义匹配器。

示例(按属性匹配对象)

import { Match } from "effect"

const input = { name: "John", age: 30 }

// Create a matcher for the specific object
const result = Match.value(input).pipe(
  // Match when the 'name' property is "John"
  Match.when(
    { name: "John" },
    (user) => `${user.name} is ${user.age} years old`,
  ),
  // Provide a fallback if no match is found
  Match.orElse(() => "Oh, not John"),
)

console.log(result)
result // => "John is 30 years old"

强制返回类型

你可以使用 Match.withReturnType<T>() 来确保所有分支都返回特定的类型。

示例(校验返回类型的一致性)

这个示例强制要求每个匹配分支都返回 string

import { Match } from "effect"

const match = Match.type<{ a: number } | { b: string }>().pipe(
  // Ensure all branches return a string
  Match.withReturnType<string>(),
  // ❌ Type error: returns a number
  // @errors: 2322
  Match.when({ a: Match.number }, (_) => _.a),
  // ✅ Correct: returns a string
  Match.when({ b: Match.string }, (_) => _.b),
  Match.exhaustive,
)
Must Be First in the Pipeline

Match.withReturnType<T>() 调用必须是管道中的第一条指令。如果放在后面,TypeScript 将无法正确强制返回类型的一致性。

定义模式

when

Match.when 函数允许你定义用于匹配值的条件。它同时支持直接的值比较和谓词函数。

示例(用值和谓词进行匹配)

import { Match } from "effect"

// Create a matcher for objects with an "age" property
const match = Match.type<{ age: number }>().pipe(
  // Match when age is greater than 18
  Match.when({ age: (age) => age > 18 }, (user) => `Age: ${user.age}`),
  // Match when age is exactly 18
  Match.when({ age: 18 }, () => "You can vote"),
  // Fallback case for all other ages
  Match.orElse((user) => `${user.age} is too young`),
)

console.log(match({ age: 20 }))
match({ age: 20 }) // => "Age: 20"

console.log(match({ age: 18 }))
match({ age: 18 }) // => "You can vote"

console.log(match({ age: 4 }))
match({ age: 4 }) // => "4 is too young"

not

Match.not 函数允许你排除特定的值,同时匹配其余所有值。

示例(忽略某个特定的值)

import { Match } from "effect"

// Create a matcher for string or number values
const match = Match.type<string | number>().pipe(
  // Match any value except "hi", returning "ok"
  Match.not("hi", () => "ok"),
  // Fallback case for when the value is "hi"
  Match.orElse(() => "fallback"),
)

console.log(match("hello"))
match("hello") // => "ok"

console.log(match("hi"))
match("hi") // => "fallback"

tag

Match.tag 函数允许基于可辨识联合中的 _tag 字段进行模式匹配。你可以在单个模式中指定多个要匹配的 tag。

示例(按 tag 匹配可辨识联合)

import { Match } from "effect"

type Event =
  | { readonly _tag: "fetch" }
  | { readonly _tag: "success"; readonly data: string }
  | { readonly _tag: "error"; readonly error: Error }
  | { readonly _tag: "cancel" }

// Create a matcher for Event
const match = Match.type<Event>().pipe(
  // Match either "fetch" or "success"
  Match.tag("fetch", "success", () => `Ok!`),
  // Match "error" and extract the error message
  Match.tag("error", (event) => `Error: ${event.error.message}`),
  // Match "cancel"
  Match.tag("cancel", () => "Cancelled"),
  Match.exhaustive,
)

console.log(match({ _tag: "success", data: "Hello" }))
match({ _tag: "success", data: "Hello" }) // => "Ok!"

console.log(match({ _tag: "error", error: new Error("Oops!") }))
match({ _tag: "error", error: new Error("Oops!") }) // => "Error: Oops!"
Tag Field Naming Convention

Match.tag 函数依赖 Effect 生态中的一项约定:把 tag 字段命名为 "_tag"。 请确保你的可辨识联合遵循这一命名约定,以保证功能正常。

内置谓词

Match 模块为常见类型提供了内置谓词,例如 Match.numberMatch.stringMatch.boolean。这些谓词简化了针对原始类型的匹配过程。

示例(对属性键使用内置谓词)

import { Match } from "effect"

const matchPropertyKey = Match.type<PropertyKey>().pipe(
  // Match when the value is a number
  Match.when(Match.number, (n) => `Key is a number: ${n}`),
  // Match when the value is a string
  Match.when(Match.string, (s) => `Key is a string: ${s}`),
  // Match when the value is a symbol
  Match.when(Match.symbol, (s) => `Key is a symbol: ${String(s)}`),
  // Ensure all possible cases are handled
  Match.exhaustive,
)

console.log(matchPropertyKey(42))
matchPropertyKey(42) // => "Key is a number: 42"

console.log(matchPropertyKey("username"))
matchPropertyKey("username") // => "Key is a string: username"

console.log(matchPropertyKey(Symbol("id")))
matchPropertyKey(Symbol("id")) // => "Key is a symbol: Symbol(id)"
谓词说明
Match.string匹配 string 类型的值。
Match.nonEmptyString匹配非空字符串。
Match.number匹配 number 类型的值。
Match.boolean匹配 boolean 类型的值。
Match.bigint匹配 bigint 类型的值。
Match.symbol匹配 symbol 类型的值。
Match.date匹配 Date 的实例值。
Match.record匹配键为 stringsymbol、值为 unknown 的对象。
Match.null匹配值 null
Match.undefined匹配值 undefined
Match.defined匹配任何已定义(非 null 且非 undefined)的值。
Match.any匹配任意值,不做限制。
Match.is(...values)匹配一组特定的字面量值(例如 Match.is("a", 42, true))。
Match.instanceOf(Class)匹配给定类的实例。

完成匹配

exhaustive

Match.exhaustive 方法通过确保所有可能的情况都已被覆盖,来终结模式匹配过程。如果有任何情况缺失,TypeScript 会产生类型错误。这在处理联合类型时特别有用,因为它有助于避免模式匹配中出现意外的遗漏。

示例(确保覆盖所有情况)

import { Match } from "effect"

// Create a matcher for string or number values
const match = Match.type<string | number>().pipe(
  // Match when the value is a number
  Match.when(Match.number, (n) => `number: ${n}`),
  // Mark the match as exhaustive, ensuring all cases are handled
  // TypeScript will throw an error if any case is missing
  // @errors: 2345
  Match.exhaustive,
)

orElse

Match.orElse 方法定义当其他模式都不匹配时返回的 fallback 值。这确保匹配器始终能产出一个有效结果。

示例(在没有模式匹配时提供默认值)

import { Match } from "effect"

// Create a matcher for string or number values
const match = Match.type<string | number>().pipe(
  // Match when the value is "a"
  Match.when("a", () => "ok"),
  // Fallback when no patterns match
  Match.orElse(() => "fallback"),
)

console.log(match("a"))
match("a") // => "ok"

console.log(match("b"))
match("b") // => "fallback"

option

Match.option 会把匹配结果包装进一个 Option。如果找到匹配,它会返回 Some(value);否则返回 None

示例(用 Option 提取用户角色)

import { Match, Option } from "effect"

type User = { readonly role: "admin" | "editor" | "viewer" }

// Create a matcher to extract user roles
const getRole = Match.type<User>().pipe(
  Match.when({ role: "admin" }, () => "Has full access"),
  Match.when({ role: "editor" }, () => "Can edit content"),
  Match.option, // Wrap the result in an Option
)

console.log(getRole({ role: "admin" }))
getRole({ role: "admin" }) // => Option.some("Has full access")

console.log(getRole({ role: "viewer" }))
getRole({ role: "viewer" }) // => Option.none()

result

Match.result 方法会把结果包装进一个 Result,提供一种结构化的方式来区分匹配与未匹配的情况。如果找到匹配,它会返回 Success(value);否则返回 Failure(no match)

示例(用 Result 提取用户角色)

import { Match, Result } from "effect"

type User = { readonly role: "admin" | "editor" | "viewer" }

// Create a matcher to extract user roles
const getRole = Match.type<User>().pipe(
  Match.when({ role: "admin" }, () => "Has full access"),
  Match.when({ role: "editor" }, () => "Can edit content"),
  Match.result, // Wrap the result in a Result
)

console.log(getRole({ role: "admin" }))
getRole({ role: "admin" }) // => Result.succeed("Has full access")

console.log(getRole({ role: "viewer" }))
getRole({ role: "viewer" }) // => Result.fail({ role: "viewer" })