Code Style by andrueandersoncs/claude-skill-effect-ts
npx skills add https://github.com/andrueandersoncs/claude-skill-effect-ts --skill 'Code Style'Effect 的惯用风格围绕三个核心原则:
其他模式包括:
绝不使用命令式结构。 所有代码必须遵循函数式编程原则:
else if 链、嵌套 if 语句、三元运算符if/else:单个 if 带可选的 (无嵌套,无 )广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
elseelse ifswitch/case 作为最后手段:优先使用 Match.type/Match.value,但当 Match 不适用时,switch 是可接受的for、while、do...while、for...of、for...in改用以下方式:
Match、Option.match、Either.match、Array.match)Array 模块(Array.map、Array.filter、Array.reduce、Array.flatMap、Array.filterMap 等)Record 模块(Record.map、Record.filter、Record.get、Record.keys、Record.values 等)Struct 模块(Struct.pick、Struct.omit、Struct.evolve、Struct.get 等)Tuple 模块(Tuple.make、Tuple.getFirst、Tuple.mapBoth 等)Predicate 模块(Predicate.and、Predicate.or、Predicate.not、Predicate.struct 等)Effect.forEach、Effect.all、Effect.reduce)Function 模块(pipe、flow、identity、constant、compose)简单if/else → 允许(无嵌套,无 else if)
else if 链 → Match.value + Match.when(禁止使用 else if)
嵌套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
// ✅ 允许:简单 if/else(无嵌套,无 else if) if (user.isAdmin) { return grantFullAccess() } return grantLimitedAccess()
// ✅ 允许:带 else 的简单 if if (isValid) { process(data) } else { handleError() }
// ❌ 禁止:else if(改用 Match) if (user.role === "admin") { return "full access" } else if (user.role === "user") { return "limited access" } else { return "no access" }
// ❌ 禁止:嵌套 if if (user.isActive) { if (user.isAdmin) { return "active admin" } }
// ❌ 禁止:三元运算符 const message = isError ? "Failed" : "Success"
// ❌ 禁止:直接访问 ._tag if (event._tag === "UserCreated") { ... } const isCreated = event._tag === "UserCreated"
// ❌ 禁止:在类型定义中使用 ._tag type ConflictTag = Conflict["_tag"] // 永远不要将 _tag 提取为类型
// ❌ 禁止:在数组谓词中使用 ._tag const hasConflict = conflicts.some((c) => c._tag === "MergeConflict") const mergeConflicts = conflicts.filter((c) => c._tag === "MergeConflict") const countMerge = conflicts.filter((c) => c._tag === "MergeConflict").length
// ✅ 要求:使用 Schema.is() 作为谓词 const hasConflict = conflicts.some(Schema.is(MergeConflict)) const mergeConflicts = conflicts.filter(Schema.is(MergeConflict)) const countMerge = conflicts.filter(Schema.is(MergeConflict)).length
// ✅ 要求:使用 Match.value 替代 else-if const getAccess = (user: User) => Match.value(user.role).pipe( Match.when("admin", () => "full access"), Match.when("user", () => "limited access"), Match.orElse(() => "no access") )
// ✅ 要求:多条件情况使用 Match.type const getStatusMessage = Match.type<Status>().pipe( Match.when("pending", () => "waiting"), Match.when("active", () => "running"), Match.exhaustive )
// ✅ 允许:switch 作为最后手段(优先使用 Match) switch (status) { case "pending": return "waiting" case "active": return "running" default: return "unknown" }
// ✅ 要求:单个可选值使用 Option.match const displayName = Option.match(maybeUser, { onNone: () => "Guest", onSome: (user) => user.name })
当你在现有代码中遇到else if 链、嵌套 if 语句或三元运算符时,立即重构它们。 简单的 if/else 是可接受的。
绝不使用for、while、do...while、for...of 或 for...in 循环。 使用 Effect 的函数式替代方案。
为什么使用 Effect 的 Array 模块而不是原生 Array 方法:
Array.findFirst 返回 Option<A> 而不是 A | undefined
Array.get 返回 Option<A> 用于安全索引
Array.filterMap 在一次遍历中结合过滤和映射
Array.partition 返回类型化元组 [excluded, satisfying]
Array.groupBy 返回 Record<K, NonEmptyArray<A>>
Array.match 提供详尽的空/非空处理
所有函数都适用于 pipe 以实现可组合的管道
一致的双重 API(数据优先和数据后置)
import { Array, pipe } from "effect"
// ❌ 禁止:for 循环 const doubled = [] for (let i = 0; i < numbers.length; i++) { doubled.push(numbers[i] * 2) }
// ❌ 禁止:for...of 循环 const results = [] for (const item of items) { results.push(process(item)) }
// ❌ 禁止:while 循环 let sum = 0 let i = 0 while (i < numbers.length) { sum += numbers[i] i++ }
// ❌ 禁止:带可变状态的 forEach const output = [] items.forEach(item => output.push(transform(item)))
// ✅ 要求:转换使用 Array.map const doubled = Array.map(numbers, (n) => n * 2) // 或使用 pipe const doubled = pipe(numbers, Array.map((n) => n * 2))
// ✅ 要求:选择使用 Array.filter const adults = Array.filter(users, (u) => u.age >= 18)
// ✅ 要求:累加使用 Array.reduce const sum = Array.reduce(numbers, 0, (acc, n) => acc + n)
// ✅ 要求:一对多使用 Array.flatMap const allTags = Array.flatMap(posts, (post) => post.tags)
// ✅ 要求:搜索使用 Array.findFirst(返回 Option) const admin = Array.findFirst(users, (u) => u.role === "admin")
// ✅ 要求:谓词使用 Array.some/every const hasAdmin = Array.some(users, (u) => u.role === "admin") const allVerified = Array.every(users, (u) => u.verified)
// ✅ 要求:一次遍历中过滤+转换使用 Array.filterMap const validEmails = Array.filterMap(users, (u) => isValidEmail(u.email) ? Option.some(u.email) : Option.none() )
// ✅ 要求:按谓词分割使用 Array.partition const [minors, adults] = Array.partition(users, (u) => u.age >= 18)
// ✅ 要求:分组使用 Array.groupBy const usersByRole = Array.groupBy(users, (u) => u.role)
// ✅ 要求:去重使用 Array.dedupe const uniqueIds = Array.dedupe(ids)
// ✅ 要求:空 vs 非空处理使用 Array.match
const message = Array.match(items, {
onEmpty: () => "No items",
onNonEmpty: (items) => ${items.length} items
})
Record 操作 - 使用 Effect 的 Record 模块:
import { Record, pipe } from "effect"
// ✅ 要求:转换值使用 Record.map
const doubled = Record.map(prices, (price) => price * 2)
// ✅ 要求:过滤条目使用 Record.filter
const expensive = Record.filter(prices, (price) => price > 100)
// ✅ 要求:安全访问使用 Record.get(返回 Option)
const price = Record.get(prices, "item1")
// ✅ 要求:使用 Record.keys 和 Record.values
const allKeys = Record.keys(config)
const allValues = Record.values(config)
// ✅ 要求:使用 Record.fromEntries 和 Record.toEntries
const record = Record.fromEntries([["a", 1], ["b", 2]])
const entries = Record.toEntries(record)
// ✅ 要求:过滤+转换使用 Record.filterMap
const validPrices = Record.filterMap(rawPrices, (value) =>
typeof value === "number" ? Option.some(value) : Option.none()
)
Struct 操作 - 使用 Effect 的 Struct 模块:
import { Struct, pipe } from "effect"
// ✅ 要求:选择属性使用 Struct.pick
const namePart = Struct.pick(user, "firstName", "lastName")
// ✅ 要求:排除属性使用 Struct.omit
const publicUser = Struct.omit(user, "password", "ssn")
// ✅ 要求:转换特定字段使用 Struct.evolve
const updated = Struct.evolve(user, {
age: (age) => age + 1,
name: (name) => name.toUpperCase()
})
// ✅ 要求:属性访问使用 Struct.get
const getName = Struct.get("name")
const name = getName(user)
Tuple 操作 - 使用 Effect 的 Tuple 模块:
import { Tuple } from "effect"
// ✅ 要求:创建元组使用 Tuple.make
const pair = Tuple.make("key", 42)
// ✅ 要求:访问使用 Tuple.getFirst/getSecond
const key = Tuple.getFirst(pair)
const value = Tuple.getSecond(pair)
// ✅ 要求:转换使用 Tuple.mapFirst/mapSecond/mapBoth
const upperKey = Tuple.mapFirst(pair, (s) => s.toUpperCase())
const doubled = Tuple.mapSecond(pair, (n) => n * 2)
const both = Tuple.mapBoth(pair, {
onFirst: (s) => s.toUpperCase(),
onSecond: (n) => n * 2
})
// ✅ 要求:索引访问使用 Tuple.at
const first = Tuple.at(tuple, 0)
Predicate 操作 - 使用 Effect 的 Predicate 模块:
import { Predicate } from "effect"
// ✅ 要求:组合谓词使用 Predicate.and/or/not
const isPositive = (n: number) => n > 0
const isEven = (n: number) => n % 2 === 0
const isPositiveAndEven = Predicate.and(isPositive, isEven)
const isPositiveOrEven = Predicate.or(isPositive, isEven)
const isNegative = Predicate.not(isPositive)
// ✅ 要求:验证对象形状使用 Predicate.struct
const isValidUser = Predicate.struct({
name: Predicate.isString,
age: Predicate.isNumber
})
// ✅ 要求:验证元组形状使用 Predicate.tuple
const isStringNumberPair = Predicate.tuple(Predicate.isString, Predicate.isNumber)
// ✅ 要求:内置类型守卫
Predicate.isString(value)
Predicate.isNumber(value)
Predicate.isNullable(value)
Predicate.isNotNullable(value)
Predicate.isRecord(value)
Effect 循环 - 使用 Effect 组合器:
// ❌ 禁止:使用 yield* 的 for...of
const processAll = Effect.gen(function* () {
const results = []
for (const item of items) {
const result = yield* processItem(item)
results.push(result)
}
return results
})
// ✅ 要求:顺序处理使用 Effect.forEach
const processAll = Effect.forEach(items, processItem)
// ✅ 要求:并行处理使用 Effect.all(当项是 Effects 时)
const results = Effect.all(effects)
// ✅ 要求:带并发限制使用 Effect.all
const results = Effect.all(effects, { concurrency: 10 })
// ✅ 要求:累加使用 Effect.reduce
const total = Effect.reduce(items, 0, (acc, item) =>
getPrice(item).pipe(Effect.map((price) => acc + price))
)
// ✅ 要求:大型/无限序列使用 Stream
const processed = Stream.fromIterable(items).pipe(
Stream.mapEffect(processItem),
Stream.runCollect
)
复杂迭代使用递归:
// ❌ 禁止:树遍历使用 while 循环
const collectLeaves = (node) => {
const leaves = []
const stack = [node]
while (stack.length > 0) {
const current = stack.pop()
if (current.children.length === 0) {
leaves.push(current)
} else {
stack.push(...current.children)
}
}
return leaves
}
// ✅ 要求:树遍历使用递归
const collectLeaves = (node: TreeNode): ReadonlyArray<TreeNode> =>
Array.match(node.children, {
onEmpty: () => [node],
onNonEmpty: (children) => Array.flatMap(children, collectLeaves)
})
// ✅ 要求:带 Effect 的递归
const processTree = (node: TreeNode): Effect.Effect<Result> =>
node.children.length === 0
? processLeaf(node)
: Effect.forEach(node.children, processTree).pipe(
Effect.flatMap(combineResults)
)
一等函数 - 使用 Effect 的 Function 模块:
import { Array, Function, pipe, flow } from "effect"
// ❌ 不好:重复的内联逻辑
const processUsers = (users: Array<User>) =>
users.filter((u) => u.active).map((u) => u.email)
const processOrders = (orders: Array<Order>) =>
orders.filter((o) => o.active).map((o) => o.total)
// ✅ 好:提取可重用的谓词和转换器
const isActive = <T extends { active: boolean }>(item: T) => item.active
const getEmail = (user: User) => user.email
const getTotal = (order: Order) => order.total
// ✅ 好:数据转换管道使用 pipe
const processUsers = (users: Array<User>) =>
pipe(users, Array.filter(isActive), Array.map(getEmail))
const processOrders = (orders: Array<Order>) =>
pipe(orders, Array.filter(isActive), Array.map(getTotal))
// ✅ 好:组合可重用管道使用 flow
const getActiveEmails = flow(
Array.filter(isActive<User>),
Array.map(getEmail)
)
const getActiveTotals = flow(
Array.filter(isActive<Order>),
Array.map(getTotal)
)
// ✅ 好:简单组合使用 Function.compose
const parseAndValidate = Function.compose(parse, validate)
// ✅ 好:直通使用 Function.identity
const transform = shouldTransform ? myTransform : Function.identity
// ✅ 好:固定值使用 Function.constant
const getDefaultUser = Function.constant(defaultUser)
当你在现有代码中遇到命令式循环时,立即重构它们。 这不是可选的 - 命令式逻辑是必须消除的代码异味。
将所有数据结构定义为 Effect Schema。 这是类型安全 Effect 代码的基础。
关键原则:
使用 Schema.Class 而不是 Schema.Struct - 获取方法和 Schema.is() 类型守卫
使用标记联合而不是可选属性 - 使状态显式化
在 Match 模式中使用 Schema.is() - 将验证与匹配结合
import { Schema, Match } from "effect";
// ✅ 好:带方法的基于类的 Schema class User extends Schema.Class<User>("User")({ id: Schema.String.pipe(Schema.brand("UserId")), email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+.[^@]+$/)), name: Schema.String.pipe(Schema.nonEmptyString()), createdAt: Schema.Date, }) { get emailDomain() { return this.email.split("@")[1]; } }
// ✅ 好:标记联合优于可选属性 class Pending extends Schema.TaggedClass<Pending>()("Pending", { orderId: Schema.String, items: Schema.Array(Schema.String), }) {}
class Shipped extends Schema.TaggedClass<Shipped>()("Shipped", { orderId: Schema.String, items: Schema.Array(Schema.String), trackingNumber: Schema.String, shippedAt: Schema.Date, }) {}
class Delivered extends Schema.TaggedClass<Delivered>()("Delivered", { orderId: Schema.String, items: Schema.Array(Schema.String), deliveredAt: Schema.Date, }) {}
const Order = Schema.Union(Pending, Shipped, Delivered); type Order = Schema.Schema.Type<typeof Order>;
// ✅ 好:在 Match 模式中使用 Schema.is()
const getOrderStatus = (order: Order) =>
Match.value(order).pipe(
Match.when(Schema.is(Pending), () => "Awaiting shipment"),
Match.when(Schema.is(Shipped), (o) => Tracking: ${o.trackingNumber}),
Match.when(Schema.is(Delivered), (o) => Delivered ${o.deliveredAt}),
Match.exhaustive,
);
// ❌ 不好:可选属性隐藏状态复杂性 const Order = Schema.Struct({ orderId: Schema.String, items: Schema.Array(Schema.String), trackingNumber: Schema.optional(Schema.String), // 何时设置? shippedAt: Schema.optional(Schema.Date), // 状态不明确 deliveredAt: Schema.optional(Schema.Date), // 可以同时是已发货和已交付吗? });
为什么所有内容都使用 Schema:
使用 Effect Match 定义所有条件逻辑和算法。 用详尽的模式匹配替换 if/else 链、switch 语句和三元运算符。
import { Match } from "effect";
// 处理可区分联合 - 使用 Match
const handleEvent = Match.type<AppEvent>().pipe(
Match.tag("UserCreated", (event) => notifyAdmin(event.userId)),
Match.tag("UserDeleted", (event) => cleanupData(event.userId)),
Match.tag("OrderPlaced", (event) => processOrder(event.orderId)),
Match.exhaustive,
);
// 转换值 - 使用 Match
const toHttpStatus = Match.type<AppError>().pipe(
Match.tag("NotFound", () => 404),
Match.tag("Unauthorized", () => 401),
Match.tag("ValidationError", () => 400),
Match.tag("InternalError", () => 500),
Match.exhaustive,
);
// 处理可选值 - 使用 Option.match
const displayUser = (maybeUser: Option<User>) =>
Option.match(maybeUser, {
onNone: () => "Guest user",
onSome: (user) => `Welcome, ${user.name}`
});
// 多条件逻辑 - 使用 Match.when
const calculateDiscount = (order: Order) =>
Match.value(order).pipe(
Match.when({ total: (t) => t > 1000, isPremium: true }, () => 0.25),
Match.when({ total: (t) => t > 1000 }, () => 0.15),
Match.when({ isPremium: true }, () => 0.1),
Match.when({ itemCount: (c) => c > 10 }, () => 0.05),
Match.orElse(() => 0),
);
为什么所有内容都使用 Match:
每个 Schema 都通过Arbitrary.make() 生成测试数据。 这是 Effect 测试的基础 - 永远不要手动创建测试对象。
关键:有关全面的测试指导,请参阅测试技能。 本节涵盖 Schema 驱动的 Arbitrary 模式;测试技能涵盖
@effect/vitest、it.effect、it.prop、测试层和完整的面向服务测试模式。
关键原则:
.filter() - 改用正确约束的 Schema@effect/vitest 的it.prop - 直接集成 Schema ArbitraryFast-check 过滤器(.filter())丢弃不匹配谓词的生成值。这是:
正确的方法:在 Schema 中定义约束,然后 Arbitrary.make() 直接生成有效值。
import { Schema, Arbitrary } from "effect"
import * as fc from "fast-check"
// ❌ 禁止:使用 fast-check 过滤器
const badArbitrary = fc.integer().filter((n) => n >= 18 && n <= 100)
// 问题:生成整数,丢弃 99% 的值
// ❌ 禁止:在 Schema Arbitrary 上过滤
const UserArbitrary = Arbitrary.make(User)
const badFiltered = UserArbitrary(fc).filter((u) => u.age >= 18)
// 问题:重复约束逻辑,浪费生成
// ✅ 要求:在 Schema 定义中约束
const Age = Schema.Number.pipe(
Schema.int(),
Schema.between(18, 100) // 约束内置于 Schema 中
)
class User extends Schema.Class<User>("User")({
id: Schema.String.pipe(Schema.minLength(1)),
name: Schema.String.pipe(Schema.nonEmptyString()),
age: Age // 使用受约束的 Age Schema
}) {}
// ✅ 要求:Arbitrary 仅生成有效值
const UserArbitrary = Arbitrary.make(User)
fc.sample(UserArbitrary(fc), 5)
// 所有 5 个用户的年龄都在 18-100 之间,保证
使用这些 Schema 组合器来约束生成(绝不使用过滤器):
import { Schema } from "effect"
// 数字约束
Schema.Number.pipe(Schema.int()) // 仅整数
Schema.Number.pipe(Schema.positive()) // > 0
Schema.Number.pipe(Schema.nonNegative()) // >= 0
Schema.Number.pipe(Schema.between(1, 100)) // 1 <= n <= 100
Schema.Number.pipe(Schema.greaterThan(0)) // > 0
Schema.Number.pipe(Schema.lessThanOrEqualTo(100)) // <= 100
// 字符串约束
Schema.String.pipe(Schema.minLength(1)) // 非空
Schema.String.pipe(Schema.maxLength(100)) // 最多 100 个字符
Schema.String.pipe(Schema.length(10)) // 正好 10 个字符
Schema.String.pipe(Schema.nonEmptyString()) // 非空(别名)
Schema.String.pipe(Schema.pattern(/^[A-Z]{3}$/)) // 匹配正则表达式
// 数组约束
Schema.Array(Item).pipe(Schema.minItems(1)) // 非空数组
Schema.Array(Item).pipe(Schema.maxItems(10)) // 最多 10 个项
Schema.Array(Item).pipe(Schema.itemsCount(5)) // 正好 5 个项
Schema.NonEmptyArray(Item) // 非空数组
// 内置约束类型
Schema.NonEmptyString // 带 minLength(1) 的字符串
Schema.Positive // 数字 > 0
Schema.NonNegative // 数字 >= 0
Schema.Int // 整数
对于 Schema 组合器无法表达的复杂约束,使用 arbitrary 注解:
import { Schema, Arbitrary } from "effect"
import * as fc from "fast-check"
// 自定义电子邮件生成(模式太复杂无法生成)
const Email = Schema.String.pipe(
Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/),
Schema.annotations({
arbitrary: () => (fc) => fc.emailAddress() // 使用 fast-check 的生成器
})
)
// 自定义 UUID 生成
const UserId = Schema.String.pipe(
Schema.annotations({
arbitrary: () => (fc) => fc.uuid()
})
)
// 自定义日期范围
const BirthDate = Schema.Date.pipe(
Schema.annotations({
arbitrary: () => (fc) =>
fc.date({
min: new Date("1900-01-01"),
max: new Date("2010-01-01")
})
})
)
使用 @effect/vitest 的 it.prop 进行基于属性的测试(完整细节请参阅测试技能):
import { it, expect } from "@effect/vitest"
import { Schema, Arbitrary } from "effect"
// 数组形式
it.prop("validates all generated users", [Arbitrary.make(User)], ([user]) => {
expect(user.age).toBeGreaterThanOrEqual(18)
expect(user.name.length).toBeGreaterThan(0)
})
// 对象形式
it.prop(
"round-trip preserves data",
{ user: Arbitrary.make(User) },
({ user }) => {
const encoded = Schema.encodeSync(User)(user)
const decoded = Schema.decodeUnknownSync(User)(encoded)
expect(decoded).toEqual(user)
}
)
// 基于 Effect 的属性测试
it.effect.prop(
"processes all order states",
[Arbitrary.make(Order)],
([order]) =>
Effect.gen(function* () {
const result = yield* processOrder(order)
expect(result).toBeDefined()
})
)
最强大的模式:数据使用 TaggedClass,逻辑使用 Match 中的 Schema.is()。
import { Schema, Match } from "effect";
// 使用 TaggedClass 定义所有变体(不是 Struct)
class CreditCard extends Schema.TaggedClass<CreditCard>()("CreditCard", {
last4: Schema.String,
expiryMonth: Schema.Number,
expiryYear: Schema.Number,
}) {
get isExpired() {
const now = new Date();
return (
this.expiryYear < now.getFullYear() ||
(this.expiryYear === now.getFullYear() && this.expiryMonth < now.getMonth() + 1)
);
}
}
class BankTransfer extends Schema.TaggedClass<BankTransfer>()("BankTransfer", {
accountId: Schema.String,
routingNumber: Schema.String,
}) {}
class Crypto extends Schema.TaggedClass<Crypto>()("Crypto", {
walletAddress: Schema.String,
network: Schema.Literal("ethereum", "bitcoin", "solana"),
}) {}
const PaymentMethod = Schema.Union(CreditCard, BankTransfer, Crypto);
type PaymentMethod = Schema.Schema.Type<typeof PaymentMethod>;
// 使用 Schema.is() 处理以访问类方法
const processPayment = (method: PaymentMethod, amount: number) =>
Match.value(method).pipe(
Match.when(Schema.is(CreditCard), (card) =>
card.isExpired ? Effect.fail("Card expired") : chargeCard(card.last4, amount),
),
Match.when(Schema.is(BankTransfer), (bank) => initiateBankTransfer(bank.accountId, bank.routingNumber, amount)),
Match.when(Schema.is(Crypto), (crypto) => sendCrypto(crypto.walletAddress, crypto.network, amount)),
Match.exhaustive,
);
// 简单情况也可使用 Match.tag
const getPaymentLabel = (method: PaymentMethod) =>
Match.value(method).pipe(
Match.tag("CreditCard", (c) => `Card ending ${c.last4}`),
Match.tag("BankTransfer", (b) => `Bank ${b.accountId}`),
Match.tag("Crypto", (c) => `${c.network}: ${c.walletAddress.slice(0, 8)}...`),
Match.exhaustive,
);
防止混淆相同基础类型的值:
import { Brand } from "effect";
// 定义品牌化类型
type UserId = string & Brand.Brand<"UserId">;
type OrderId = string & Brand.Brand<"OrderId">;
// 构造函数
const UserId = Brand.nominal<UserId>();
const OrderId = Brand.nominal<OrderId>();
// 使用
const userId: UserId = UserId("user-123");
const orderId: OrderId = OrderId("order-456");
// 类型错误:不能将 UserId 赋值给 OrderId
// const wrong: OrderId = userId
import { Brand, Either } from "effect";
type Email = string & Brand.Brand<"Email">;
const Email = Brand.refined<Email>(
(s) => /^[^@]+@[^@]+\.[^@]+$/.test(s),
(s) => Brand.error(`Invalid email: ${s}`),
);
// 返回 Either
const result = Email.either("test@example.com");
// 或抛出
const email = Email("test@example.com");
import { Schema } from "effect";
const UserId = Schema.String.pipe(Schema.brand("UserId"));
Effect's idiomatic style centers on three core principles:
Additional patterns include:
NEVER use imperative constructs. All code must follow functional programming principles:
else if chains, nested if statements, ternary operatorsif/else is allowed: A single if with optional else (no nesting, no else if)switch/case as last resort: Prefer Match.type/Match.value, but switch is acceptable when Match doesn't fitfor, while, do...while, for...of, for...inUse instead:
Match, Option.match, Either.match, Array.match)Array module (Array.map, Array.filter, Array.reduce, Array.flatMap, Array.filterMap, etc.)Record module (, , , , , etc.)Simpleif/else → Allowed (no nesting, no else if)
else if chains → Match.value + Match.when (FORBIDDEN as else if)
Nestedif statements → Flatten with early return, or Match.value + Match.when
statements → Prefer + , but is acceptable
When you encounterelse if chains, nested if statements, or ternary operators in existing code, refactor them immediately. Simple if/else is acceptable.
NEVER usefor, while, do...while, for...of, or for...in loops. Use Effect's functional alternatives.
Why Effect's Array module over native Array methods:
Array.findFirst returns Option<A> instead of A | undefined
Array.get returns Option<A> for safe indexing
Array.filterMap combines filter and map in one pass
Array.partition returns typed tuple [excluded, satisfying]
Array.groupBy returns Record<K, NonEmptyArray<A>>
Record operations - use Effect's Record module:
import { Record, pipe } from "effect"
// ✅ REQUIRED: Record.map for transforming values
const doubled = Record.map(prices, (price) => price * 2)
// ✅ REQUIRED: Record.filter for filtering entries
const expensive = Record.filter(prices, (price) => price > 100)
// ✅ REQUIRED: Record.get for safe access (returns Option)
const price = Record.get(prices, "item1")
// ✅ REQUIRED: Record.keys and Record.values
const allKeys = Record.keys(config)
const allValues = Record.values(config)
// ✅ REQUIRED: Record.fromEntries and Record.toEntries
const record = Record.fromEntries([["a", 1], ["b", 2]])
const entries = Record.toEntries(record)
// ✅ REQUIRED: Record.filterMap for filter + transform
const validPrices = Record.filterMap(rawPrices, (value) =>
typeof value === "number" ? Option.some(value) : Option.none()
)
Struct operations - use Effect's Struct module:
import { Struct, pipe } from "effect"
// ✅ REQUIRED: Struct.pick for selecting properties
const namePart = Struct.pick(user, "firstName", "lastName")
// ✅ REQUIRED: Struct.omit for excluding properties
const publicUser = Struct.omit(user, "password", "ssn")
// ✅ REQUIRED: Struct.evolve for transforming specific fields
const updated = Struct.evolve(user, {
age: (age) => age + 1,
name: (name) => name.toUpperCase()
})
// ✅ REQUIRED: Struct.get for property access
const getName = Struct.get("name")
const name = getName(user)
Tuple operations - use Effect's Tuple module:
import { Tuple } from "effect"
// ✅ REQUIRED: Tuple.make for creating tuples
const pair = Tuple.make("key", 42)
// ✅ REQUIRED: Tuple.getFirst/getSecond for access
const key = Tuple.getFirst(pair)
const value = Tuple.getSecond(pair)
// ✅ REQUIRED: Tuple.mapFirst/mapSecond/mapBoth for transformation
const upperKey = Tuple.mapFirst(pair, (s) => s.toUpperCase())
const doubled = Tuple.mapSecond(pair, (n) => n * 2)
const both = Tuple.mapBoth(pair, {
onFirst: (s) => s.toUpperCase(),
onSecond: (n) => n * 2
})
// ✅ REQUIRED: Tuple.at for indexed access
const first = Tuple.at(tuple, 0)
Predicate operations - use Effect's Predicate module:
import { Predicate } from "effect"
// ✅ REQUIRED: Predicate.and/or/not for combining predicates
const isPositive = (n: number) => n > 0
const isEven = (n: number) => n % 2 === 0
const isPositiveAndEven = Predicate.and(isPositive, isEven)
const isPositiveOrEven = Predicate.or(isPositive, isEven)
const isNegative = Predicate.not(isPositive)
// ✅ REQUIRED: Predicate.struct for validating object shapes
const isValidUser = Predicate.struct({
name: Predicate.isString,
age: Predicate.isNumber
})
// ✅ REQUIRED: Predicate.tuple for validating tuple shapes
const isStringNumberPair = Predicate.tuple(Predicate.isString, Predicate.isNumber)
// ✅ REQUIRED: Built-in type guards
Predicate.isString(value)
Predicate.isNumber(value)
Predicate.isNullable(value)
Predicate.isNotNullable(value)
Predicate.isRecord(value)
Effect loops - use Effect combinators:
// ❌ FORBIDDEN: for...of with yield*
const processAll = Effect.gen(function* () {
const results = []
for (const item of items) {
const result = yield* processItem(item)
results.push(result)
}
return results
})
// ✅ REQUIRED: Effect.forEach for sequential
const processAll = Effect.forEach(items, processItem)
// ✅ REQUIRED: Effect.all for parallel (when items are Effects)
const results = Effect.all(effects)
// ✅ REQUIRED: Effect.all with concurrency
const results = Effect.all(effects, { concurrency: 10 })
// ✅ REQUIRED: Effect.reduce for accumulation
const total = Effect.reduce(items, 0, (acc, item) =>
getPrice(item).pipe(Effect.map((price) => acc + price))
)
// ✅ REQUIRED: Stream for large/infinite sequences
const processed = Stream.fromIterable(items).pipe(
Stream.mapEffect(processItem),
Stream.runCollect
)
Recursion for complex iteration:
// ❌ FORBIDDEN: while loop for tree traversal
const collectLeaves = (node) => {
const leaves = []
const stack = [node]
while (stack.length > 0) {
const current = stack.pop()
if (current.children.length === 0) {
leaves.push(current)
} else {
stack.push(...current.children)
}
}
return leaves
}
// ✅ REQUIRED: Recursion for tree traversal
const collectLeaves = (node: TreeNode): ReadonlyArray<TreeNode> =>
Array.match(node.children, {
onEmpty: () => [node],
onNonEmpty: (children) => Array.flatMap(children, collectLeaves)
})
// ✅ REQUIRED: Recursion with Effect
const processTree = (node: TreeNode): Effect.Effect<Result> =>
node.children.length === 0
? processLeaf(node)
: Effect.forEach(node.children, processTree).pipe(
Effect.flatMap(combineResults)
)
First-class functions - use Effect's Function module:
import { Array, Function, pipe, flow } from "effect"
// ❌ BAD: Inline logic repeated
const processUsers = (users: Array<User>) =>
users.filter((u) => u.active).map((u) => u.email)
const processOrders = (orders: Array<Order>) =>
orders.filter((o) => o.active).map((o) => o.total)
// ✅ GOOD: Extract reusable predicates and transformers
const isActive = <T extends { active: boolean }>(item: T) => item.active
const getEmail = (user: User) => user.email
const getTotal = (order: Order) => order.total
// ✅ GOOD: Use pipe for data transformation pipelines
const processUsers = (users: Array<User>) =>
pipe(users, Array.filter(isActive), Array.map(getEmail))
const processOrders = (orders: Array<Order>) =>
pipe(orders, Array.filter(isActive), Array.map(getTotal))
// ✅ GOOD: Use flow to compose reusable pipelines
const getActiveEmails = flow(
Array.filter(isActive<User>),
Array.map(getEmail)
)
const getActiveTotals = flow(
Array.filter(isActive<Order>),
Array.map(getTotal)
)
// ✅ GOOD: Use Function.compose for simple composition
const parseAndValidate = Function.compose(parse, validate)
// ✅ GOOD: Use Function.identity for pass-through
const transform = shouldTransform ? myTransform : Function.identity
// ✅ GOOD: Use Function.constant for fixed values
const getDefaultUser = Function.constant(defaultUser)
When you encounter imperative loops in existing code, refactor them immediately. This is not optional - imperative logic is a code smell that must be eliminated.
Define ALL data structures as Effect Schemas. This is the foundation of type-safe Effect code.
Key principles:
Use Schema.Class over Schema.Struct - Get methods and Schema.is() type guards
Use tagged unions over optional properties - Make states explicit
Use Schema.is() in Match patterns - Combine validation with matching
import { Schema, Match } from "effect";
// ✅ GOOD: Class-based schema with methods class User extends Schema.Class<User>("User")({ id: Schema.String.pipe(Schema.brand("UserId")), email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+.[^@]+$/)), name: Schema.String.pipe(Schema.nonEmptyString()), createdAt: Schema.Date, }) { get emailDomain() { return this.email.split("@")[1]; } }
// ✅ GOOD: Tagged union over optional properties class Pending extends Schema.TaggedClass<Pending>()("Pending", { orderId: Schema.String, items: Schema.Array(Schema.String), }) {}
class Shipped extends Schema.TaggedClass<Shipped>()("Shipped", { orderId: Schema.String, items: Schema.Array(Schema.String), trackingNumber: Schema.String, shippedAt: Schema.Date, }) {}
class Delivered extends Schema.TaggedClass<Delivered>()("Delivered", { orderId: Schema.String, items: Schema.Array(Schema.String), deliveredAt: Schema.Date, }) {}
const Order = Schema.Union(Pending, Shipped, Delivered); type Order = Schema.Schema.Type<typeof Order>;
// ✅ GOOD: Schema.is() in Match patterns
const getOrderStatus = (order: Order) =>
Match.value(order).pipe(
Match.when(Schema.is(Pending), () => "Awaiting shipment"),
Match.when(Schema.is(Shipped), (o) => Tracking: ${o.trackingNumber}),
Match.when(Schema.is(Delivered), (o) => Delivered ${o.deliveredAt}),
Match.exhaustive,
);
// ❌ BAD: Optional properties hide state complexity const Order = Schema.Struct({ orderId: Schema.String, items: Schema.Array(Schema.String), trackingNumber: Schema.optional(Schema.String), // When is this set? shippedAt: Schema.optional(Schema.Date), // Unclear state deliveredAt: Schema.optional(Schema.Date), // Can be shipped AND delivered? });
Why Schema for everything:
Define ALL conditional logic and algorithms using Effect Match. Replace if/else chains, switch statements, and ternaries with exhaustive pattern matching.
import { Match } from "effect";
// Process by discriminated union - use Match
const handleEvent = Match.type<AppEvent>().pipe(
Match.tag("UserCreated", (event) => notifyAdmin(event.userId)),
Match.tag("UserDeleted", (event) => cleanupData(event.userId)),
Match.tag("OrderPlaced", (event) => processOrder(event.orderId)),
Match.exhaustive,
);
// Transform values - use Match
const toHttpStatus = Match.type<AppError>().pipe(
Match.tag("NotFound", () => 404),
Match.tag("Unauthorized", () => 401),
Match.tag("ValidationError", () => 400),
Match.tag("InternalError", () => 500),
Match.exhaustive,
);
// Handle options - use Option.match
const displayUser = (maybeUser: Option<User>) =>
Option.match(maybeUser, {
onNone: () => "Guest user",
onSome: (user) => `Welcome, ${user.name}`
});
// Multi-condition logic - use Match.when
const calculateDiscount = (order: Order) =>
Match.value(order).pipe(
Match.when({ total: (t) => t > 1000, isPremium: true }, () => 0.25),
Match.when({ total: (t) => t > 1000 }, () => 0.15),
Match.when({ isPremium: true }, () => 0.1),
Match.when({ itemCount: (c) => c > 10 }, () => 0.05),
Match.orElse(() => 0),
);
Why Match for everything:
Every Schema generates test data viaArbitrary.make(). This is the foundation of Effect testing — never hand-craft test objects.
CRITICAL: See theTesting Skill for comprehensive testing guidance. This section covers Schema-driven Arbitrary patterns; the Testing skill covers
@effect/vitest,it.effect,it.prop, test layers, and the full service-oriented testing pattern.
Key principles:
.filter() — Use properly-constrained Schemas insteadit.prop from @effect/vitest — Integrates Schema Arbitrary directlyFast-check filters (.filter()) discard generated values that don't match the predicate. This is:
The correct approach: define constraints in your Schema , then Arbitrary.make() generates valid values directly.
import { Schema, Arbitrary } from "effect"
import * as fc from "fast-check"
// ❌ FORBIDDEN: Using fast-check filter
const badArbitrary = fc.integer().filter((n) => n >= 18 && n <= 100)
// Problem: Generates integers, throws away 99% of them
// ❌ FORBIDDEN: Filter on Schema Arbitrary
const UserArbitrary = Arbitrary.make(User)
const badFiltered = UserArbitrary(fc).filter((u) => u.age >= 18)
// Problem: Duplicates constraint logic, wasteful generation
// ✅ REQUIRED: Constraints in Schema definition
const Age = Schema.Number.pipe(
Schema.int(),
Schema.between(18, 100) // Constraint built into Schema
)
class User extends Schema.Class<User>("User")({
id: Schema.String.pipe(Schema.minLength(1)),
name: Schema.String.pipe(Schema.nonEmptyString()),
age: Age // Uses constrained Age schema
}) {}
// ✅ REQUIRED: Arbitrary generates ONLY valid values
const UserArbitrary = Arbitrary.make(User)
fc.sample(UserArbitrary(fc), 5)
// All 5 users have age between 18-100, guaranteed
Use these Schema combinators to constrain generation (never filter):
import { Schema } from "effect"
// Numeric constraints
Schema.Number.pipe(Schema.int()) // Integers only
Schema.Number.pipe(Schema.positive()) // > 0
Schema.Number.pipe(Schema.nonNegative()) // >= 0
Schema.Number.pipe(Schema.between(1, 100)) // 1 <= n <= 100
Schema.Number.pipe(Schema.greaterThan(0)) // > 0
Schema.Number.pipe(Schema.lessThanOrEqualTo(100)) // <= 100
// String constraints
Schema.String.pipe(Schema.minLength(1)) // Non-empty
Schema.String.pipe(Schema.maxLength(100)) // Max 100 chars
Schema.String.pipe(Schema.length(10)) // Exactly 10 chars
Schema.String.pipe(Schema.nonEmptyString()) // Non-empty (alias)
Schema.String.pipe(Schema.pattern(/^[A-Z]{3}$/)) // Matches regex
// Array constraints
Schema.Array(Item).pipe(Schema.minItems(1)) // Non-empty array
Schema.Array(Item).pipe(Schema.maxItems(10)) // Max 10 items
Schema.Array(Item).pipe(Schema.itemsCount(5)) // Exactly 5 items
Schema.NonEmptyArray(Item) // Non-empty array
// Built-in constrained types
Schema.NonEmptyString // String with minLength(1)
Schema.Positive // Number > 0
Schema.NonNegative // Number >= 0
Schema.Int // Integer
For complex constraints that Schema combinators can't express, use arbitrary annotation:
import { Schema, Arbitrary } from "effect"
import * as fc from "fast-check"
// Custom email generation (pattern too complex for generation)
const Email = Schema.String.pipe(
Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/),
Schema.annotations({
arbitrary: () => (fc) => fc.emailAddress() // Use fast-check's generator
})
)
// Custom UUID generation
const UserId = Schema.String.pipe(
Schema.annotations({
arbitrary: () => (fc) => fc.uuid()
})
)
// Custom date range
const BirthDate = Schema.Date.pipe(
Schema.annotations({
arbitrary: () => (fc) =>
fc.date({
min: new Date("1900-01-01"),
max: new Date("2010-01-01")
})
})
)
Use it.prop from @effect/vitest for property-based tests (see Testing Skill for full details):
import { it, expect } from "@effect/vitest"
import { Schema, Arbitrary } from "effect"
// Array form
it.prop("validates all generated users", [Arbitrary.make(User)], ([user]) => {
expect(user.age).toBeGreaterThanOrEqual(18)
expect(user.name.length).toBeGreaterThan(0)
})
// Object form
it.prop(
"round-trip preserves data",
{ user: Arbitrary.make(User) },
({ user }) => {
const encoded = Schema.encodeSync(User)(user)
const decoded = Schema.decodeUnknownSync(User)(encoded)
expect(decoded).toEqual(user)
}
)
// Effect-based property test
it.effect.prop(
"processes all order states",
[Arbitrary.make(Order)],
([order]) =>
Effect.gen(function* () {
const result = yield* processOrder(order)
expect(result).toBeDefined()
})
)
The most powerful pattern: TaggedClass for data, Schema.is() in Match for logic.
import { Schema, Match } from "effect";
// Define all variants with TaggedClass (not Struct)
class CreditCard extends Schema.TaggedClass<CreditCard>()("CreditCard", {
last4: Schema.String,
expiryMonth: Schema.Number,
expiryYear: Schema.Number,
}) {
get isExpired() {
const now = new Date();
return (
this.expiryYear < now.getFullYear() ||
(this.expiryYear === now.getFullYear() && this.expiryMonth < now.getMonth() + 1)
);
}
}
class BankTransfer extends Schema.TaggedClass<BankTransfer>()("BankTransfer", {
accountId: Schema.String,
routingNumber: Schema.String,
}) {}
class Crypto extends Schema.TaggedClass<Crypto>()("Crypto", {
walletAddress: Schema.String,
network: Schema.Literal("ethereum", "bitcoin", "solana"),
}) {}
const PaymentMethod = Schema.Union(CreditCard, BankTransfer, Crypto);
type PaymentMethod = Schema.Schema.Type<typeof PaymentMethod>;
// Process with Schema.is() to access class methods
const processPayment = (method: PaymentMethod, amount: number) =>
Match.value(method).pipe(
Match.when(Schema.is(CreditCard), (card) =>
card.isExpired ? Effect.fail("Card expired") : chargeCard(card.last4, amount),
),
Match.when(Schema.is(BankTransfer), (bank) => initiateBankTransfer(bank.accountId, bank.routingNumber, amount)),
Match.when(Schema.is(Crypto), (crypto) => sendCrypto(crypto.walletAddress, crypto.network, amount)),
Match.exhaustive,
);
// Also works with Match.tag for simple cases
const getPaymentLabel = (method: PaymentMethod) =>
Match.value(method).pipe(
Match.tag("CreditCard", (c) => `Card ending ${c.last4}`),
Match.tag("BankTransfer", (b) => `Bank ${b.accountId}`),
Match.tag("Crypto", (c) => `${c.network}: ${c.walletAddress.slice(0, 8)}...`),
Match.exhaustive,
);
Prevent mixing up values of the same underlying type:
import { Brand } from "effect";
// Define branded types
type UserId = string & Brand.Brand<"UserId">;
type OrderId = string & Brand.Brand<"OrderId">;
// Constructors
const UserId = Brand.nominal<UserId>();
const OrderId = Brand.nominal<OrderId>();
// Usage
const userId: UserId = UserId("user-123");
const orderId: OrderId = OrderId("order-456");
// Type error: can't assign UserId to OrderId
// const wrong: OrderId = userId
import { Brand, Either } from "effect";
type Email = string & Brand.Brand<"Email">;
const Email = Brand.refined<Email>(
(s) => /^[^@]+@[^@]+\.[^@]+$/.test(s),
(s) => Brand.error(`Invalid email: ${s}`),
);
// Returns Either
const result = Email.either("test@example.com");
// Or throws
const email = Email("test@example.com");
import { Schema } from "effect";
const UserId = Schema.String.pipe(Schema.brand("UserId"));
type UserId = Schema.Schema.Type<typeof UserId>;
const Email = Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/), Schema.brand("Email"));
Most Effect functions support both styles:
import { Effect, pipe } from "effect";
// Using pipe
const result = pipe(
Effect.succeed(1),
Effect.map((n) => n + 1),
Effect.flatMap((n) => Effect.succeed(n * 2)),
);
// Using method chaining
const result = Effect.succeed(1).pipe(
Effect.map((n) => n + 1),
Effect.flatMap((n) => Effect.succeed(n * 2)),
);
// Useful for single transformations
const mapped = Effect.map(Effect.succeed(1), (n) => n + 1);
The preferred way to write sequential Effect code:
// Generator style - recommended
const program = Effect.gen(function* () {
const user = yield* getUser(id);
const orders = yield* getOrders(user.id);
const enriched = yield* enrichOrders(orders);
return { user, orders: enriched };
});
// Equivalent flatMap chain
const program = getUser(id).pipe(
Effect.flatMap((user) =>
getOrders(user.id).pipe(
Effect.flatMap((orders) => enrichOrders(orders).pipe(Effect.map((enriched) => ({ user, orders: enriched })))),
),
),
);
Alternative to generators for some cases:
import { Effect } from "effect";
const program = Effect.Do.pipe(
Effect.bind("user", () => getUser(id)),
Effect.bind("orders", ({ user }) => getOrders(user.id)),
Effect.bind("enriched", ({ orders }) => enrichOrders(orders)),
Effect.map(({ user, enriched }) => ({ user, orders: enriched })),
);
src/
├── domain/ # Domain types and errors
│ ├── User.ts
│ ├── Order.ts
│ └── errors.ts
├── services/ # Service interfaces
│ ├── UserRepository.ts
│ └── OrderService.ts
├── implementations/ # Service implementations
│ ├── UserRepositoryLive.ts
│ └── OrderServiceLive.ts
├── layers/ # Layer composition
│ ├── AppLive.ts
│ └── TestLive.ts
├── http/ # HTTP handlers
│ └── routes.ts
└── main.ts # Entry point
// services/UserRepository.ts
import { Context, Effect } from "effect";
import { User, UserId } from "../domain/User";
import { UserNotFound } from "../domain/errors";
export class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{
readonly findById: (id: UserId) => Effect.Effect<User, UserNotFound>;
readonly findByEmail: (email: string) => Effect.Effect<User, UserNotFound>;
readonly save: (user: User) => Effect.Effect<void>;
readonly delete: (id: UserId) => Effect.Effect<void>;
}
>() {}
// layers/AppLive.ts
import { Layer } from "effect";
// Infrastructure
const InfraLive = Layer.mergeAll(DatabaseLive, HttpClientLive, LoggerLive);
// Repositories
const RepositoriesLive = Layer.mergeAll(UserRepositoryLive, OrderRepositoryLive).pipe(Layer.provide(InfraLive));
// Services
const ServicesLive = Layer.mergeAll(UserServiceLive, OrderServiceLive).pipe(Layer.provide(RepositoriesLive));
// Full application
export const AppLive = ServicesLive;
// Domain types - PascalCase
interface User { ... }
interface Order { ... }
// Branded types - PascalCase
type UserId = string & Brand.Brand<"UserId">
type Email = string & Brand.Brand<"Email">
// Error types - PascalCase with descriptive suffix
class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", {...}) {}
class ValidationError extends Schema.TaggedError<ValidationError>()("ValidationError", {...}) {}
// Service tag - PascalCase
class UserRepository extends Context.Tag("UserRepository")<...>() {}
// Layer implementations - PascalCase with Live/Test suffix
const UserRepositoryLive = Layer.effect(...)
const UserRepositoryTest = Layer.succeed(...)
// Effect-returning functions - camelCase
const getUser = (id: UserId): Effect.Effect<User, UserNotFound> => ...
const createOrder = (data: OrderData): Effect.Effect<Order, ValidationError> => ...
// Constructors - matching type name
const UserId = Brand.nominal<UserId>()
const User = (data: UserData): User => ...
import { Schema } from "effect";
// Always use Schema.TaggedError for domain errors
class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", { userId: Schema.String }) {}
class ValidationError extends Schema.TaggedError<ValidationError>()("ValidationError", {
field: Schema.String,
message: Schema.String,
}) {}
// Use in services
const getUser = (id: string): Effect.Effect<User, UserNotFound> =>
Effect.gen(function* () {
const user = yield* findInDb(id);
if (!user) {
return yield* Effect.fail(new UserNotFound({ userId: id }));
}
return user;
});
const program = getUser(id).pipe(
// Specific error handling
Effect.catchTag("UserNotFound", (error) => Effect.succeed(defaultUser)),
// Or match all errors
Effect.catchTags({
UserNotFound: () => Effect.succeed(defaultUser),
ValidationError: (e) => Effect.fail(new BadRequest(e.message)),
}),
);
else if, nested if, ternaries, for/while loops (simple if/else is OK)Array, Record, Struct, Tuple for data manipulationPredicate.and, Predicate.or, Predicate.struct for composing predicatesContext.Tag service. Direct external calls make code untestable.else if - use Match.value + Match.whenif statements - flatten with early return, or use Matchswitch/case - but switch is acceptable as last resortif (x != null) - always use Option.matchOption.match calls - use Option.flatMap chains with when all branches return the same defaultThis skill covers high-level patterns and conventions. For detailed API usage and specific topics, consult these specialized skills:
CRITICAL: Always consult theTesting Skill when writing tests. It covers the full service-oriented testing pattern,
@effect/vitestAPIs (it.effect,it.prop,it.layer), test layers, and achieving 100% test coverage.
| Skill | Purpose | Key Topics |
|---|---|---|
| Testing | Effect testing patterns | @effect/vitest, it.effect, it.prop, test layers, service mocking, Arbitrary |
| Effect Core | Core Effect type and APIs | Creating Effects, Effect.gen, pipe, map, flatMap, running Effects |
| Error Management | Typed error handling | catchTag, catchAll, mapError, orElse, error accumulation |
| Pattern Matching | Match module APIs | Match.value, Match.type, Match.tag, Match.when, exhaustive matching |
| Schema | Data modeling and validation |
These skills work together: this Code Style skill defines the what (patterns to follow), while the specialized skills define the how (API details).
For comprehensive code style 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
Next.js 15+ 最佳实践指南:文件约定、RSC边界、异步模式与性能优化
928 周安装
// ❌ 禁止:嵌套 Option.match(末日金字塔) // 信号:所有 onNone 返回相同的默认值 const name = pipe( findUser(id), Option.match({ onNone: () => "Unknown", onSome: (user) => Option.match(user.profile, { onNone: () => "Unknown", onSome: (profile) => profile.displayName, }), }), )
// ✅ 要求:多个可选值使用 Option.flatMap 链 const name = pipe( findUser(id), Option.flatMap((user) => user.profile), Option.map((profile) => profile.displayName), Option.getOrElse(() => "Unknown"), )
// ✅ 要求:结果使用 Either.match
const result = Either.match(parseResult, {
onLeft: (error) => Error: ${error},
onRight: (value) => Success: ${value}
})
// ✅ 要求:可区分联合使用 Match.tag(而不是 ._tag 访问) const handleEvent = Match.type<AppEvent>().pipe( Match.tag("UserCreated", (e) => notifyAdmin(e.userId)), Match.tag("UserDeleted", (e) => cleanupData(e.userId)), Match.exhaustive )
// ✅ 要求:Schema 类型的类型守卫使用 Schema.is()(Schema.TaggedClass) if (Schema.is(UserCreated)(event)) { // event 被缩小为 UserCreated }
// Schema.TaggedError 可与 Schema.is()、Effect.catchTag 和 Match.tag 一起使用。 // 领域错误始终使用 Schema.TaggedError。
Record.mapRecord.filterRecord.getRecord.keysRecord.valuesStruct module (Struct.pick, Struct.omit, Struct.evolve, Struct.get, etc.)Tuple module (Tuple.make, Tuple.getFirst, Tuple.mapBoth, etc.)Predicate module (Predicate.and, Predicate.or, Predicate.not, Predicate.struct, etc.)Effect.forEach, Effect.all, Effect.reduce)Function module (pipe, flow, identity, constant, compose)switch/caseMatch.typeMatch.tagswitchTernary operators (? :) → Match.value + Match.when or simple if/else
Single optional value → Option.match
Chained optional operations → Option.flatMap + Option.getOrElse
Result/error conditionals → Either.match or Effect.match
// ✅ ALLOWED: Simple if/else (not nested, no else if) if (user.isAdmin) { return grantFullAccess() } return grantLimitedAccess()
// ✅ ALLOWED: Simple if with else if (isValid) { process(data) } else { handleError() }
// ❌ FORBIDDEN: else if (use Match instead) if (user.role === "admin") { return "full access" } else if (user.role === "user") { return "limited access" } else { return "no access" }
// ❌ FORBIDDEN: Nested if if (user.isActive) { if (user.isAdmin) { return "active admin" } }
// ❌ FORBIDDEN: ternary const message = isError ? "Failed" : "Success"
// ❌ FORBIDDEN: direct ._tag access if (event._tag === "UserCreated") { ... } const isCreated = event._tag === "UserCreated"
// ❌ FORBIDDEN: ._tag in type definitions type ConflictTag = Conflict["_tag"] // Never extract _tag as a type
// ❌ FORBIDDEN: ._tag in array predicates const hasConflict = conflicts.some((c) => c._tag === "MergeConflict") const mergeConflicts = conflicts.filter((c) => c._tag === "MergeConflict") const countMerge = conflicts.filter((c) => c._tag === "MergeConflict").length
// ✅ REQUIRED: Schema.is() as predicate const hasConflict = conflicts.some(Schema.is(MergeConflict)) const mergeConflicts = conflicts.filter(Schema.is(MergeConflict)) const countMerge = conflicts.filter(Schema.is(MergeConflict)).length
// ✅ REQUIRED: Match.value for else-if replacement const getAccess = (user: User) => Match.value(user.role).pipe( Match.when("admin", () => "full access"), Match.when("user", () => "limited access"), Match.orElse(() => "no access") )
// ✅ REQUIRED: Match.type for multi-case const getStatusMessage = Match.type<Status>().pipe( Match.when("pending", () => "waiting"), Match.when("active", () => "running"), Match.exhaustive )
// ✅ ALLOWED: switch as last resort (prefer Match) switch (status) { case "pending": return "waiting" case "active": return "running" default: return "unknown" }
// ✅ REQUIRED: Option.match for single optional const displayName = Option.match(maybeUser, { onNone: () => "Guest", onSome: (user) => user.name })
// ❌ FORBIDDEN: Nested Option.match (pyramid of doom) // Signal: every onNone returns the same default const name = pipe( findUser(id), Option.match({ onNone: () => "Unknown", onSome: (user) => Option.match(user.profile, { onNone: () => "Unknown", onSome: (profile) => profile.displayName, }), }), )
// ✅ REQUIRED: Option.flatMap chain for multiple optionals const name = pipe( findUser(id), Option.flatMap((user) => user.profile), Option.map((profile) => profile.displayName), Option.getOrElse(() => "Unknown"), )
// ✅ REQUIRED: Either.match for results
const result = Either.match(parseResult, {
onLeft: (error) => Error: ${error},
onRight: (value) => Success: ${value}
})
// ✅ REQUIRED: Match.tag for discriminated unions (not ._tag access) const handleEvent = Match.type<AppEvent>().pipe( Match.tag("UserCreated", (e) => notifyAdmin(e.userId)), Match.tag("UserDeleted", (e) => cleanupData(e.userId)), Match.exhaustive )
// ✅ REQUIRED: Schema.is() for type guards on Schema types (Schema.TaggedClass) if (Schema.is(UserCreated)(event)) { // event is narrowed to UserCreated }
// Schema.TaggedError works with Schema.is(), Effect.catchTag, and Match.tag. // Always use Schema.TaggedError for domain errors.
Array.match provides exhaustive empty/non-empty handling
All functions work with pipe for composable pipelines
Consistent dual API (data-first and data-last)
import { Array, pipe } from "effect"
// ❌ FORBIDDEN: for loop const doubled = [] for (let i = 0; i < numbers.length; i++) { doubled.push(numbers[i] * 2) }
// ❌ FORBIDDEN: for...of loop const results = [] for (const item of items) { results.push(process(item)) }
// ❌ FORBIDDEN: while loop let sum = 0 let i = 0 while (i < numbers.length) { sum += numbers[i] i++ }
// ❌ FORBIDDEN: forEach with mutation const output = [] items.forEach(item => output.push(transform(item)))
// ✅ REQUIRED: Array.map for transformation const doubled = Array.map(numbers, (n) => n * 2) // or with pipe const doubled = pipe(numbers, Array.map((n) => n * 2))
// ✅ REQUIRED: Array.filter for selection const adults = Array.filter(users, (u) => u.age >= 18)
// ✅ REQUIRED: Array.reduce for accumulation const sum = Array.reduce(numbers, 0, (acc, n) => acc + n)
// ✅ REQUIRED: Array.flatMap for one-to-many const allTags = Array.flatMap(posts, (post) => post.tags)
// ✅ REQUIRED: Array.findFirst for search (returns Option) const admin = Array.findFirst(users, (u) => u.role === "admin")
// ✅ REQUIRED: Array.some/every for predicates const hasAdmin = Array.some(users, (u) => u.role === "admin") const allVerified = Array.every(users, (u) => u.verified)
// ✅ REQUIRED: Array.filterMap for filter + transform in one pass const validEmails = Array.filterMap(users, (u) => isValidEmail(u.email) ? Option.some(u.email) : Option.none() )
// ✅ REQUIRED: Array.partition to split by predicate const [minors, adults] = Array.partition(users, (u) => u.age >= 18)
// ✅ REQUIRED: Array.groupBy for grouping const usersByRole = Array.groupBy(users, (u) => u.role)
// ✅ REQUIRED: Array.dedupe for removing duplicates const uniqueIds = Array.dedupe(ids)
// ✅ REQUIRED: Array.match for empty vs non-empty handling
const message = Array.match(items, {
onEmpty: () => "No items",
onNonEmpty: (items) => ${items.length} items
})
pipe, flow, identity, constant, composeelse if chains, nested if, ternaries (simple if/else is OK)Context.Tag services*Live for production, *Test for testing. This is required for 100% test coverage.@effect/vitest for ALL tests - it.effect, it.scoped, it.live, it.layer, it.prop (see Testing Skill)Arbitrary.make(Schema) for ALL test data - Never hand-craft test objectsSchema.between(), Schema.minLength(), etc. generate constrained values directlyOption.getOrElseonNoneArray.map/Array.filter/Array.reduce or Effect.forEachArray module or Effect combinatorsArray.append, Array.prepend, spread, or immutable operationsArray.prototype.find - use Array.findFirst which returns Optionarray[index] - use Array.get(array, index) which returns OptionObject.keys/values/entries - use Record.keys, Record.values, Record.toEntriesrecord[key] - use Record.get(record, key) which returns Option&&/|| for predicates - use Predicate.and, Predicate.or, Predicate.not.success or similar - always use Either.match or Effect.match._tag directly - always use Match.tag or Schema.is() (for Schema types only)._tag as a type - e.g., type Tag = Foo["_tag"] is forbidden._tag in predicates - use Schema.is(Variant) with .some()/.filter()cause field on error types capturing arbitrary caught exceptions, opaque pass-through payloads). If you can describe the data shape, define a proper schema instead.Effect.runPromise in tests - use it.effect from @effect/vitestit from vitest in Effect test files - import from @effect/vitestArbitrary.make(Schema) or it.prop.filter() - define constraints in Schema (Schema.between(), Schema.minLength(), etc.) so Arbitrary generates valid values directly| Schema.Class, Schema.Struct, parsing, transformations, filters |