Pattern Matching by andrueandersoncs/claude-skill-effect-ts
npx skills add https://github.com/andrueandersoncs/claude-skill-effect-ts --skill 'Pattern Matching'模式匹配用于替代 Effect 代码中的复杂控制流。 简单的 if/else(无嵌套,无 else if)是允许的,但 else if 链、嵌套条件语句和三目运算符必须使用模式匹配。
Effect 的 Match 模块提供:
| 命令式模式 | Effect 替代方案 |
|---|---|
简单的 if/else(无嵌套) |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 允许原样使用 |
else if 链 | Match.value + Match.when |
嵌套的 if 语句 | Match.value + Match.when |
switch/case 语句 | 优先使用 Match.type + Match.tag(switch 可接受) |
三目运算符(? :) | Match.value + Match.when 或简单的 if/else |
| 单次空值检查 | Option.match |
| 链式可选操作 | Option.flatMap + Option.getOrElse |
| 错误检查 | Either.match 或 Effect.match |
| 类型守卫 | 使用 Schema.is() 的 Match.when |
遇到命令式控制流时,立即将其重构为模式匹配。
import { Match } from "effect"
const result = Match.value(input).pipe(
Match.when("admin", () => "Full access"),
Match.when("user", () => "Limited access"),
Match.when("guest", () => "Read only"),
Match.exhaustive
)
const rolePermissions = Match.type<"admin" | "user" | "guest">().pipe(
Match.when("admin", () => "Full access"),
Match.when("user", () => "Limited access"),
Match.when("guest", () => "Read only"),
Match.exhaustive
)
// 多次使用
const perm1 = rolePermissions("admin")
const perm2 = rolePermissions("guest")
type Shape =
| { _tag: "Circle"; radius: number }
| { _tag: "Rectangle"; width: number; height: number }
| { _tag: "Triangle"; base: number; height: number }
const area = Match.type<Shape>().pipe(
Match.tag("Circle", ({ radius }) => Math.PI * radius ** 2),
Match.tag("Rectangle", ({ width, height }) => width * height),
Match.tag("Triangle", ({ base, height }) => (base * height) / 2),
Match.exhaustive
)
area({ _tag: "Circle", radius: 5 }) // 78.54...
type AppError =
| { _tag: "NetworkError"; url: string }
| { _tag: "ValidationError"; field: string; message: string }
| { _tag: "AuthError"; reason: string }
const handleError = Match.type<AppError>().pipe(
Match.tag("NetworkError", (e) => `Failed to fetch ${e.url}`),
Match.tag("ValidationError", (e) => `${e.field}: ${e.message}`),
Match.tag("AuthError", (e) => `Auth failed: ${e.reason}`),
Match.exhaustive
)
const describeNumber = Match.type<number>().pipe(
Match.when((n) => n < 0, () => "negative"),
Match.when((n) => n === 0, () => "zero"),
Match.when((n) => n > 0 && n < 10, () => "small positive"),
Match.when((n) => n >= 10, () => "large positive"),
Match.exhaustive
)
const processInput = Match.type<string | number | boolean>().pipe(
Match.when(
(x): x is string => typeof x === "string",
(s) => `String: ${s.toUpperCase()}`
),
Match.when(
(x): x is number => typeof x === "number",
(n) => `Number: ${n * 2}`
),
Match.when(
(x): x is boolean => typeof x === "boolean",
(b) => `Boolean: ${!b}`
),
Match.exhaustive
)
const greet = Match.type<string>().pipe(
Match.when("morning", () => "Good morning!"),
Match.when("evening", () => "Good evening!"),
Match.orElse(() => "Hello!")
)
greet("morning") // "Good morning!"
greet("afternoon") // "Hello!"
// 当你认为所有情况都已覆盖时使用
// 如果遇到未处理的情况,会在运行时抛出错误
const handle = Match.type<"a" | "b">().pipe(
Match.when("a", () => 1),
Match.when("b", () => 2),
Match.orElseAbsurd
)
const classify = Match.type<number>().pipe(
Match.when((n) => n === 0, () => "zero"),
Match.not((n) => n > 0, () => "negative"), // 当 NOT 正数时匹配
Match.orElse(() => "positive")
)
const isWeekend = Match.type<string>().pipe(
Match.whenOr("Saturday", "Sunday", () => true),
Match.orElse(() => false)
)
interface User {
role: "admin" | "user"
verified: boolean
}
const canDelete = Match.type<User>().pipe(
Match.whenAnd(
{ role: "admin" },
(u) => u.verified,
() => true
),
Match.orElse(() => false)
)
const processEvent = Match.type<Event>().pipe(
Match.when({ type: "click" }, (e) => handleClick(e)),
Match.when({ type: "keydown" }, (e) => handleKeydown(e)),
Match.when({ type: "submit" }, (e) => handleSubmit(e)),
Match.orElse(() => { /* unknown event */ })
)
interface Response {
status: number
data: { type: string; value: unknown }
}
const handleResponse = Match.type<Response>().pipe(
Match.when(
{ status: 200, data: { type: "user" } },
(r) => `User: ${r.data.value}`
),
Match.when(
{ status: 200, data: { type: "product" } },
(r) => `Product: ${r.data.value}`
),
Match.when({ status: 404 }, () => "Not found"),
Match.when({ status: 500 }, () => "Server error"),
Match.orElse(() => "Unknown response")
)
function processStatus(status: Status): string {
if (status === "pending") {
return "Waiting..."
} else if (status === "active") {
return "In progress"
} else if (status === "completed") {
return "Done!"
} else if (status === "failed") {
return "Error occurred"
} else {
return "Unknown"
}
}
const processStatus = Match.type<Status>().pipe(
Match.when("pending", () => "Waiting..."),
Match.when("active", () => "In progress"),
Match.when("completed", () => "Done!"),
Match.when("failed", () => "Error occurred"),
Match.exhaustive // 如果 status 类型改变,编译时会报错!
)
function getDiscount(userType: UserType): number {
switch (userType) {
case "regular":
return 0
case "premium":
return 10
case "vip":
return 20
default:
return 0
}
}
const getDiscount = Match.type<UserType>().pipe(
Match.when("regular", () => 0),
Match.when("premium", () => 10),
Match.when("vip", () => 20),
Match.exhaustive
)
const handleError = (error: AppError) =>
Match.value(error).pipe(
Match.tag("NetworkError", (e) =>
Effect.gen(function* () {
yield* Effect.logError("Network failure", { url: e.url })
return yield* Effect.fail(e)
})
),
Match.tag("ValidationError", (e) =>
Effect.succeed({ field: e.field, message: e.message })
),
Match.tag("AuthError", () =>
Effect.redirect("/login")
),
Match.exhaustive
)
在 Match.when 模式中使用 Schema.is() 将 Schema 验证与模式匹配结合。这适用于 Schema.TaggedClass 和其他 Schema 类型。
对领域错误使用 Schema.TaggedError - 它们可以与 Schema.is()、Effect.catchTag 和 Match.tag 配合使用:
Schema.is(ErrorClass) 对错误进行类型守卫Effect.catchTag("ErrorName", ...) 进行错误处理Match.tag("ErrorName", ...)(包括谓词)import { Schema, Match } from "effect"
// 使用 TaggedClass 定义带有方法的模式
class Circle extends Schema.TaggedClass<Circle>()("Circle", {
radius: Schema.Number
}) {
get area() { return Math.PI * this.radius ** 2 }
get circumference() { return 2 * Math.PI * this.radius }
}
class Rectangle extends Schema.TaggedClass<Rectangle>()("Rectangle", {
width: Schema.Number,
height: Schema.Number
}) {
get area() { return this.width * this.height }
get perimeter() { return 2 * (this.width + this.height) }
}
const Shape = Schema.Union(Circle, Rectangle)
type Shape = Schema.Schema.Type<typeof Shape>
// Schema.is() 提供类型守卫 + 访问类方法的能力
const describeShape = (shape: Shape) =>
Match.value(shape).pipe(
Match.when(Schema.is(Circle), (c) =>
`Circle: area=${c.area.toFixed(2)}, circumference=${c.circumference.toFixed(2)}`
),
Match.when(Schema.is(Rectangle), (r) =>
`Rectangle: area=${r.area}, perimeter=${r.perimeter}`
),
Match.exhaustive
)
// Match.tag - 更简单,当你只需要数据时使用
const getShapeName = (shape: Shape) =>
Match.value(shape).pipe(
Match.tag("Circle", () => "circle"),
Match.tag("Rectangle", () => "rectangle"),
Match.exhaustive
)
// Schema.is() - 当你需要类方法或类型收窄时使用
const processShape = (shape: Shape) =>
Match.value(shape).pipe(
Match.when(Schema.is(Circle), (c) => c.area), // 可以使用 .area 方法
Match.when(Schema.is(Rectangle), (r) => r.area), // 可以使用 .area 方法
Match.exhaustive
)
// Schema.is() 也适用于运行时验证未知数据
const handleUnknown = (input: unknown) =>
Match.value(input).pipe(
Match.when(Schema.is(Circle), (c) => `Valid circle with radius ${c.radius}`),
Match.when(Schema.is(Rectangle), (r) => `Valid rectangle ${r.width}x${r.height}`),
Match.orElse(() => "Invalid shape")
)
// 或用于类型收窄
const processInput = (input: unknown) => {
if (Schema.is(Circle)(input)) {
console.log(`Circle area: ${input.area}`) // 类型为 Circle,具有方法
}
}
import { Schema, Match, Effect } from "effect"
// 使用 TaggedClass 定义状态
class Draft extends Schema.TaggedClass<Draft>()("Draft", {
content: Schema.String
}) {
get isEmpty() { return this.content.trim().length === 0 }
}
class Published extends Schema.TaggedClass<Published>()("Published", {
content: Schema.String,
publishedAt: Schema.Date
}) {
get daysSincePublish() {
return Math.floor((Date.now() - this.publishedAt.getTime()) / 86400000)
}
}
class Archived extends Schema.TaggedClass<Archived>()("Archived", {
content: Schema.String,
archivedReason: Schema.String
}) {}
const Article = Schema.Union(Draft, Published, Archived)
type Article = Schema.Schema.Type<typeof Article>
// 使用 Schema.is() 处理以访问类方法
const getArticleStatus = (article: Article) =>
Match.value(article).pipe(
Match.when(Schema.is(Draft), (d) =>
d.isEmpty ? "Empty draft" : "Draft with content"
),
Match.when(Schema.is(Published), (p) =>
`Published ${p.daysSincePublish} days ago`
),
Match.when(Schema.is(Archived), (a) =>
`Archived: ${a.archivedReason}`
),
Match.exhaustive
)
Option.match 用于单次 Option 到值的转换。对于链式多个可选操作,使用 Option.flatMap:
// ✅ 良好:单次 Option.match(将 Option 转换为不同类型)
const greeting = Option.match(maybeUser, {
onNone: () => "Hello, guest!",
onSome: (user) => `Hello, ${user.name}!`
})
// ❌ 不佳:嵌套的 Option.match(每个 onNone 返回相同的默认值)
const result = Option.match(maybeA, {
onNone: () => fallback,
onSome: (a) =>
Option.match(maybeB(a), {
onNone: () => fallback,
onSome: (b) => transform(b),
}),
})
// ✅ 良好:Option.flatMap 链(扁平、可读、单一回退)
const result = pipe(
maybeA,
Option.flatMap(maybeB),
Option.map(transform),
Option.getOrElse(() => fallback),
)
规则: 当每个 onNone 分支返回相同的值时,这表明应该使用 Option.flatMap + Option.getOrElse 进行扁平化。
else if - 替换为 Match.value + Match.whenif 语句 - 扁平化或替换为 Matchswitch/case - 但 switch 可作为最后手段接受if (x != null) - 替换为 Option.match._tag - 替换为 Match.tag 或 Schema.is()onNone 分支共享相同的回退值时,使用带有 Option.getOrElse 的 Option.flatMap 链有关全面的模式匹配文档,请查阅 ${CLAUDE_PLUGIN_ROOT}/references/llms-full.txt。
搜索以下部分:
每周安装量
0
仓库
GitHub 星标数
5
首次出现
1970年1月1日
安全审计
Pattern matching replaces complex control flow in Effect code. Simple if/else (no nesting, no else if) is allowed, but else if chains, nested conditionals, and ternary operators must use pattern matching.
Effect's Match module provides:
| Imperative Pattern | Effect Replacement |
|---|---|
Simple if/else (no nesting) | Allowed as-is |
else if chains | Match.value + Match.when |
Nested if statements | Match.value + Match.when |
switch/case statements | Prefer Match.type + Match.tag (switch acceptable) |
Ternary operators (? :) | Match.value + Match.when or simple if/else |
| Single null check | Option.match |
| Chained optionals | Option.flatMap + Option.getOrElse |
| Error checks | Either.match or Effect.match |
| Type guards | Match.when with Schema.is() |
When you encounter imperative control flow, refactor it to pattern matching immediately.
import { Match } from "effect"
const result = Match.value(input).pipe(
Match.when("admin", () => "Full access"),
Match.when("user", () => "Limited access"),
Match.when("guest", () => "Read only"),
Match.exhaustive
)
const rolePermissions = Match.type<"admin" | "user" | "guest">().pipe(
Match.when("admin", () => "Full access"),
Match.when("user", () => "Limited access"),
Match.when("guest", () => "Read only"),
Match.exhaustive
)
// Use multiple times
const perm1 = rolePermissions("admin")
const perm2 = rolePermissions("guest")
type Shape =
| { _tag: "Circle"; radius: number }
| { _tag: "Rectangle"; width: number; height: number }
| { _tag: "Triangle"; base: number; height: number }
const area = Match.type<Shape>().pipe(
Match.tag("Circle", ({ radius }) => Math.PI * radius ** 2),
Match.tag("Rectangle", ({ width, height }) => width * height),
Match.tag("Triangle", ({ base, height }) => (base * height) / 2),
Match.exhaustive
)
area({ _tag: "Circle", radius: 5 }) // 78.54...
type AppError =
| { _tag: "NetworkError"; url: string }
| { _tag: "ValidationError"; field: string; message: string }
| { _tag: "AuthError"; reason: string }
const handleError = Match.type<AppError>().pipe(
Match.tag("NetworkError", (e) => `Failed to fetch ${e.url}`),
Match.tag("ValidationError", (e) => `${e.field}: ${e.message}`),
Match.tag("AuthError", (e) => `Auth failed: ${e.reason}`),
Match.exhaustive
)
const describeNumber = Match.type<number>().pipe(
Match.when((n) => n < 0, () => "negative"),
Match.when((n) => n === 0, () => "zero"),
Match.when((n) => n > 0 && n < 10, () => "small positive"),
Match.when((n) => n >= 10, () => "large positive"),
Match.exhaustive
)
const processInput = Match.type<string | number | boolean>().pipe(
Match.when(
(x): x is string => typeof x === "string",
(s) => `String: ${s.toUpperCase()}`
),
Match.when(
(x): x is number => typeof x === "number",
(n) => `Number: ${n * 2}`
),
Match.when(
(x): x is boolean => typeof x === "boolean",
(b) => `Boolean: ${!b}`
),
Match.exhaustive
)
const greet = Match.type<string>().pipe(
Match.when("morning", () => "Good morning!"),
Match.when("evening", () => "Good evening!"),
Match.orElse(() => "Hello!")
)
greet("morning") // "Good morning!"
greet("afternoon") // "Hello!"
// Use when you believe all cases are covered
// Throws at runtime if unhandled case reached
const handle = Match.type<"a" | "b">().pipe(
Match.when("a", () => 1),
Match.when("b", () => 2),
Match.orElseAbsurd
)
const classify = Match.type<number>().pipe(
Match.when((n) => n === 0, () => "zero"),
Match.not((n) => n > 0, () => "negative"), // Matches when NOT positive
Match.orElse(() => "positive")
)
const isWeekend = Match.type<string>().pipe(
Match.whenOr("Saturday", "Sunday", () => true),
Match.orElse(() => false)
)
interface User {
role: "admin" | "user"
verified: boolean
}
const canDelete = Match.type<User>().pipe(
Match.whenAnd(
{ role: "admin" },
(u) => u.verified,
() => true
),
Match.orElse(() => false)
)
const processEvent = Match.type<Event>().pipe(
Match.when({ type: "click" }, (e) => handleClick(e)),
Match.when({ type: "keydown" }, (e) => handleKeydown(e)),
Match.when({ type: "submit" }, (e) => handleSubmit(e)),
Match.orElse(() => { /* unknown event */ })
)
interface Response {
status: number
data: { type: string; value: unknown }
}
const handleResponse = Match.type<Response>().pipe(
Match.when(
{ status: 200, data: { type: "user" } },
(r) => `User: ${r.data.value}`
),
Match.when(
{ status: 200, data: { type: "product" } },
(r) => `Product: ${r.data.value}`
),
Match.when({ status: 404 }, () => "Not found"),
Match.when({ status: 500 }, () => "Server error"),
Match.orElse(() => "Unknown response")
)
function processStatus(status: Status): string {
if (status === "pending") {
return "Waiting..."
} else if (status === "active") {
return "In progress"
} else if (status === "completed") {
return "Done!"
} else if (status === "failed") {
return "Error occurred"
} else {
return "Unknown"
}
}
const processStatus = Match.type<Status>().pipe(
Match.when("pending", () => "Waiting..."),
Match.when("active", () => "In progress"),
Match.when("completed", () => "Done!"),
Match.when("failed", () => "Error occurred"),
Match.exhaustive // Compile error if status type changes!
)
function getDiscount(userType: UserType): number {
switch (userType) {
case "regular":
return 0
case "premium":
return 10
case "vip":
return 20
default:
return 0
}
}
const getDiscount = Match.type<UserType>().pipe(
Match.when("regular", () => 0),
Match.when("premium", () => 10),
Match.when("vip", () => 20),
Match.exhaustive
)
const handleError = (error: AppError) =>
Match.value(error).pipe(
Match.tag("NetworkError", (e) =>
Effect.gen(function* () {
yield* Effect.logError("Network failure", { url: e.url })
return yield* Effect.fail(e)
})
),
Match.tag("ValidationError", (e) =>
Effect.succeed({ field: e.field, message: e.message })
),
Match.tag("AuthError", () =>
Effect.redirect("/login")
),
Match.exhaustive
)
Use Schema.is() in Match.when patterns to combine Schema validation with pattern matching. This works with Schema.TaggedClass and other Schema types.
UseSchema.TaggedError for domain errors - they work with Schema.is(), Effect.catchTag, and Match.tag:
Schema.is(ErrorClass) for type guards on errorsEffect.catchTag("ErrorName", ...) for error handlingMatch.tag("ErrorName", ...) when matching on errors (including predicates)import { Schema, Match } from "effect"
// Define schemas with TaggedClass for methods
class Circle extends Schema.TaggedClass<Circle>()("Circle", {
radius: Schema.Number
}) {
get area() { return Math.PI * this.radius ** 2 }
get circumference() { return 2 * Math.PI * this.radius }
}
class Rectangle extends Schema.TaggedClass<Rectangle>()("Rectangle", {
width: Schema.Number,
height: Schema.Number
}) {
get area() { return this.width * this.height }
get perimeter() { return 2 * (this.width + this.height) }
}
const Shape = Schema.Union(Circle, Rectangle)
type Shape = Schema.Schema.Type<typeof Shape>
// Schema.is() provides type guard + access to class methods
const describeShape = (shape: Shape) =>
Match.value(shape).pipe(
Match.when(Schema.is(Circle), (c) =>
`Circle: area=${c.area.toFixed(2)}, circumference=${c.circumference.toFixed(2)}`
),
Match.when(Schema.is(Rectangle), (r) =>
`Rectangle: area=${r.area}, perimeter=${r.perimeter}`
),
Match.exhaustive
)
// Match.tag - simpler, when you just need the data
const getShapeName = (shape: Shape) =>
Match.value(shape).pipe(
Match.tag("Circle", () => "circle"),
Match.tag("Rectangle", () => "rectangle"),
Match.exhaustive
)
// Schema.is() - when you need class methods or type narrowing
const processShape = (shape: Shape) =>
Match.value(shape).pipe(
Match.when(Schema.is(Circle), (c) => c.area), // Can use .area method
Match.when(Schema.is(Rectangle), (r) => r.area), // Can use .area method
Match.exhaustive
)
// Schema.is() also works for runtime validation of unknown data
const handleUnknown = (input: unknown) =>
Match.value(input).pipe(
Match.when(Schema.is(Circle), (c) => `Valid circle with radius ${c.radius}`),
Match.when(Schema.is(Rectangle), (r) => `Valid rectangle ${r.width}x${r.height}`),
Match.orElse(() => "Invalid shape")
)
// Or use for type narrowing
const processInput = (input: unknown) => {
if (Schema.is(Circle)(input)) {
console.log(`Circle area: ${input.area}`) // Type is Circle, has methods
}
}
import { Schema, Match, Effect } from "effect"
// Define states with TaggedClass
class Draft extends Schema.TaggedClass<Draft>()("Draft", {
content: Schema.String
}) {
get isEmpty() { return this.content.trim().length === 0 }
}
class Published extends Schema.TaggedClass<Published>()("Published", {
content: Schema.String,
publishedAt: Schema.Date
}) {
get daysSincePublish() {
return Math.floor((Date.now() - this.publishedAt.getTime()) / 86400000)
}
}
class Archived extends Schema.TaggedClass<Archived>()("Archived", {
content: Schema.String,
archivedReason: Schema.String
}) {}
const Article = Schema.Union(Draft, Published, Archived)
type Article = Schema.Schema.Type<typeof Article>
// Process with Schema.is() to access class methods
const getArticleStatus = (article: Article) =>
Match.value(article).pipe(
Match.when(Schema.is(Draft), (d) =>
d.isEmpty ? "Empty draft" : "Draft with content"
),
Match.when(Schema.is(Published), (p) =>
`Published ${p.daysSincePublish} days ago`
),
Match.when(Schema.is(Archived), (a) =>
`Archived: ${a.archivedReason}`
),
Match.exhaustive
)
Option.match is for single Option-to-value conversion. For chaining multiple optional operations, use Option.flatMap:
// ✅ GOOD: Single Option.match (converting Option to different type)
const greeting = Option.match(maybeUser, {
onNone: () => "Hello, guest!",
onSome: (user) => `Hello, ${user.name}!`
})
// ❌ BAD: Nested Option.match (every onNone returns same default)
const result = Option.match(maybeA, {
onNone: () => fallback,
onSome: (a) =>
Option.match(maybeB(a), {
onNone: () => fallback,
onSome: (b) => transform(b),
}),
})
// ✅ GOOD: Option.flatMap chain (flat, readable, single fallback)
const result = pipe(
maybeA,
Option.flatMap(maybeB),
Option.map(transform),
Option.getOrElse(() => fallback),
)
Rule: When every onNone branch returns the same value, that's a signal to flatten with Option.flatMap + Option.getOrElse.
else if - Replace with Match.value + Match.whenif statements - Flatten or replace with Matchswitch/case - But switch is acceptable as last resortif (x != null) - Replace with Option.match._tag directly - Replace with Match.tag or Schema.is()Option.flatMap chains with Option.getOrElse when all branches share the same fallbackFor comprehensive pattern matching documentation, consult ${CLAUDE_PLUGIN_ROOT}/references/llms-full.txt.
Search for these sections:
Weekly Installs
0
Repository
GitHub Stars
5
First Seen
Jan 1, 1970
Security Audits
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
103,800 周安装
onNone