ts-best-practices-functional
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesets-best-practices-functional
—
Refactor or author TypeScript using a functional doctrine: factories over classes, Result<T,E> over exceptions, immutable state via spread/map/filter, and pure functions composed in pipelines.
Claude Code extensions (ignored by other agents) --- argument-hint: '[<file-or-dir>]' user-invocable: true model-invocable: true
When to use
ts-best-practices-functional
Verbatim trigger phrases:
- "make this functional"
- "remove the class"
- "use Result instead of throw"
- "stop mutating this"
- "refactor to factory function"
- "compose these as pure functions"
- "use immutable state"
采用函数式原则重构或编写TypeScript代码:优先使用工厂函数而非类,优先使用Result<T,E>而非异常,通过扩展运算符/spread、map/filter实现不可变状态,以及采用管道方式组合纯函数。
When NOT to use
适用场景
- The class wraps a stateful external SDK (,
PrismaClient, WebSocket connections) — keep itOctokit - Framework requires a class (legacy React class components, custom Error subclasses)
- User wants general TS hygiene → use
ts-best-practices - Working in plain JS without TypeScript
明确触发短语:
- "make this functional"(转为函数式实现)
- "remove the class"(移除类)
- "use Result instead of throw"(用Result替代throw)
- "stop mutating this"(停止修改此对象)
- "refactor to factory function"(重构为工厂函数)
- "compose these as pure functions"(将这些组合为纯函数)
- "use immutable state"(使用不可变状态)
Core principles
不适用场景
| Prefer | Over | Why |
|---|---|---|
| Data transformations | Mutations | Predictable, easier to reason about |
| Functions | Methods | No |
| Composition | Inheritance | Mix behaviors without coupling |
| Explicit | Implicit | State passed in, not hidden |
| Factories | Classes | Closure-encapsulated state, no |
- 类用于封装有状态的外部SDK(、
PrismaClient、WebSocket连接)——保留类Octokit - 框架要求必须使用类(传统React类组件、自定义Error子类)
- 用户需要通用TypeScript代码规范 → 使用
ts-best-practices - 处理纯JavaScript代码而非TypeScript
Patterns
核心原则
Factories over classes
—
Use a factory function returning an interface to encapsulate state. Closures make the state truly private; no to bind.
<good>
interface Counter {
increment: () => number
decrement: () => number
getValue: () => number
}
thisfunction createCounter(initial: number = 0): Counter {
let value = initial
return {
increment: () => ++value,
decrement: () => --value,
getValue: () => value,
}
}
const counter = createCounter(10)
counter.increment() // 11
</good>
<bad>
class Counter {
count = 0
increment() { this.count++ }
}
const c = new Counter()
const fn = c.increment
fn() // TypeError — is lost!
</bad>
this| 优先选择 | 而非 | 原因 |
|---|---|---|
| 数据转换 | 修改操作 | 可预测性更强,更易理解 |
| 函数 | 方法 | 无 |
| 组合模式 | 继承 | 无需耦合即可混合行为 |
| 显式实现 | 隐式实现 | 状态传入而非隐藏 |
| 工厂函数 | 类 | 通过闭包封装状态,无需 |
Factory advantages
模式
—
优先使用工厂函数而非类
- No confusion
this - No keyword
new - Easy to test (just call the function)
- Can return different implementations based on env/config
- Private state via closure (truly inaccessible from outside)
ts
// Factory returning different implementations
function createLogger(env: 'dev' | 'prod') {
if (env === 'dev') {
return { log: (msg: string) => console.log(`[DEV] ${msg}`) }
}
return { log: (msg: string) => sendToLogService(msg) }
}使用返回接口的工厂函数封装状态。闭包可实现真正的私有状态;无需绑定。
<good>
interface Counter {
increment: () => number
decrement: () => number
getValue: () => number
}
thisfunction createCounter(initial: number = 0): Counter {
let value = initial
return {
increment: () => ++value,
decrement: () => --value,
getValue: () => value,
}
}
const counter = createCounter(10)
counter.increment() // 11
</good>
<bad>
class Counter {
count = 0
increment() { this.count++ }
}
const c = new Counter()
const fn = c.increment
fn() // TypeError — 丢失!
</bad>
thisImmutability by default
工厂函数的优势
Never mutate arrays or objects passed in. Return new state.
<good>
function addItem(items: Item[], newItem: Item): Item[] {
return [...items, newItem]
}
function updateItem(items: Item[], id: string, updates: Partial<Item>): Item[] {
return items.map((item) =>
item.id === id ? { ...item, ...updates } : item
)
}
function removeItem(items: Item[], id: string): Item[] {
return items.filter((item) => item.id !== id)
}
</good>
<bad>
const items: Item[] = []
function addItem(item: Item) {
items.push(item) // mutates outer state!
}
function updateItem(id: string, updates: Partial<Item>) {
const item = items.find((i) => i.id === id)
Object.assign(item, updates) // mutates the item!
}
</bad>
Use modifiers and to enforce at the type level:
readonlyas constts
function processItems(items: readonly Item[]): readonly Item[] {
return items.filter((item) => item.active)
}
const STATUSES = ['pending', 'active', 'done'] as const
type Status = (typeof STATUSES)[number]- 无混淆问题
this - 无需关键字
new - 易于测试(直接调用函数即可)
- 可根据环境/配置返回不同实现
- 通过闭包实现私有状态(外部无法访问)
ts
// 根据环境返回不同实现的工厂函数
function createLogger(env: 'dev' | 'prod') {
if (env === 'dev') {
return { log: (msg: string) => console.log(`[DEV] ${msg}`) }
}
return { log: (msg: string) => sendToLogService(msg) }
}Result<T,E> over exceptions
默认采用不可变模式
For expected failure modes (parsing, validation, network, I/O), return a instead of throwing. Errors become part of the type signature.
Result<T, E>ts
interface Ok<T> {
readonly ok: true
readonly value: T
}
interface Err<E> {
readonly ok: false
readonly error: E
}
type Result<T, E = Error> = Ok<T> | Err<E>
const ok = <T>(value: T): Ok<T> => ({ ok: true, value })
const err = <E>(error: E): Err<E> => ({ ok: false, error })function parseConfig(json: string): Result<Config, ParseError> {
try {
return ok(JSON.parse(json))
} catch {
return err({ type: 'invalid_json', message: 'Invalid JSON' })
}
}
const result = parseConfig(input)
if (!result.ok) {
logger.warn({ error: result.error }, 'parse failed')
return
}
processConfig(result.value) // typed as Config
</good>
<bad>
function parseConfig(json: string): Config {
return JSON.parse(json) // throws on bad input — caller doesn't know
}
// caller forgets to try/catch
const config = parseConfig(input) // crashes the request
</bad>
永远不要修改传入的数组或对象。返回新的状态。
<good>
function addItem(items: Item[], newItem: Item): Item[] {
return [...items, newItem]
}
function updateItem(items: Item[], id: string, updates: Partial<Item>): Item[] {
return items.map((item) =>
item.id === id ? { ...item, ...updates } : item
)
}
function removeItem(items: Item[], id: string): Item[] {
return items.filter((item) => item.id !== id)
}
</good>
<bad>
const items: Item[] = []
function addItem(item: Item) {
items.push(item) // 修改外部状态!
}
function updateItem(id: string, updates: Partial<Item>) {
const item = items.find((i) => i.id === id)
Object.assign(item, updates) // 修改item对象!
}
</bad>
使用修饰符和在类型层面强制执行不可变:
readonlyas constts
function processItems(items: readonly Item[]): readonly Item[] {
return items.filter((item) => item.active)
}
const STATUSES = ['pending', 'active', 'done'] as const
type Status = (typeof STATUSES)[number]When Result is and isn't appropriate
优先使用Result<T,E>而非异常
| Use Result | Don't use Result |
|---|---|
| JSON parsing, validation | Truly exceptional errors (out-of-memory) |
| External API calls | Programming bugs (assertion failures) |
| File I/O, network | Internal invariants that should never fail |
| Business logic with known failure modes | Operations with no realistic failure |
对于预期的失败场景(解析、验证、网络、I/O操作),返回而非抛出异常。错误将成为类型签名的一部分。
Result<T, E>ts
interface Ok<T> {
readonly ok: true
readonly value: T
}
interface Err<E> {
readonly ok: false
readonly error: E
}
type Result<T, E = Error> = Ok<T> | Err<E>
const ok = <T>(value: T): Ok<T> => ({ ok: true, value })
const err = <E>(error: E): Err<E> => ({ ok: false, error })function parseConfig(json: string): Result<Config, ParseError> {
try {
return ok(JSON.parse(json))
} catch {
return err({ type: 'invalid_json', message: '无效JSON' })
}
}
const result = parseConfig(input)
if (!result.ok) {
logger.warn({ error: result.error }, '解析失败')
return
}
processConfig(result.value) // 类型为Config
</good>
<bad>
function parseConfig(json: string): Config {
return JSON.parse(json) // 输入错误时抛出异常——调用方不知情
}
// 调用方忘记添加try/catch
const config = parseConfig(input) // 导致请求崩溃
</bad>
Async pattern
Result的适用与不适用场景
ts
async function attemptAsync<T, E = unknown>(fn: () => Promise<T>): Promise<Result<T, E>> {
try {
return ok(await fn())
} catch (error) {
return err(error as E)
}
}
const result = await attemptAsync(() => fetch('/api/users'))
if (!result.ok) return logger.error('fetch failed')
const response = result.value| 适用Result的场景 | 不适用Result的场景 |
|---|---|
| JSON解析、验证 | 真正异常的错误(如内存不足) |
| 外部API调用 | 编程错误(断言失败) |
| 文件I/O、网络操作 | 内部不变量(理论上永远不会失效) |
| 存在已知失败模式的业务逻辑 | 无实际失败可能的操作 |
Domain-specific error types
异步模式
Define error types per domain — generic loses information.
Errorts
interface ApiError {
type: 'network' | 'timeout' | 'unauthorized' | 'not_found' | 'server_error'
message: string
statusCode?: number
}
async function fetchUser(id: UserId): Promise<Result<User, ApiError>> {
// ...
}ts
async function attemptAsync<T, E = unknown>(fn: () => Promise<T>): Promise<Result<T, E>> {
try {
return ok(await fn())
} catch (error) {
return err(error as E)
}
}
const result = await attemptAsync(() => fetch('/api/users'))
if (!result.ok) return logger.error('获取失败')
const response = result.valuePure functions + composition
领域特定错误类型
Pure functions: same inputs → same outputs, no side effects (no I/O, no global state changes, no mutation of arguments).
ts
// pure
function calculateTotal(items: readonly Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0)
}
// impure — side effects
function calculateTotal(items: Item[]): number {
console.log('Calculating...') // side effect: I/O
analytics.track('total_calculated') // side effect: external state
return items.reduce((s, i) => s + i.price, 0)
}Isolate side effects at the edges of the application:
ts
// pure business logic
function validateUser(user: User): Result<User, ValidationError> {
// ...
}
// side effects at the edge
async function handleUserCreate(user: User) {
const validation = validateUser(user) // pure
if (!validation.ok) {
logger.warn({ validation }, 'invalid user') // I/O at edge
return
}
await db.user.create(validation.value) // I/O at edge
}Compose small pure functions:
ts
const normalize = (s: string) => s.trim().toLowerCase()
const validate = (s: string) => s.length > 0
const format = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
function processName(input: string): string | null {
const normalized = normalize(input)
if (!validate(normalized)) return null
return format(normalized)
}为每个领域定义错误类型——通用会丢失信息。
Errorts
interface ApiError {
type: 'network' | 'timeout' | 'unauthorized' | 'not_found' | 'server_error'
message: string
statusCode?: number
}
async function fetchUser(id: UserId): Promise<Result<User, ApiError>> {
// ...
}Derived state, not duplicated
纯函数 + 组合模式
Compute derived values from source state. Don't store them.
<good>
interface CartState {
items: readonly CartItem[]
}
function getTotal(state: CartState): number {
return state.items.reduce((sum, item) => sum + item.price, 0)
}
function getItemCount(state: CartState): number {
return state.items.length
}
</good>
<bad>
interface CartState {
items: CartItem[]
total: number // gets out of sync with items
itemCount: number // gets out of sync with items
}
</bad>纯函数:相同输入→相同输出,无副作用(无I/O操作、无全局状态修改、无参数修改)。
ts
// 纯函数
function calculateTotal(items: readonly Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0)
}
// 非纯函数——存在副作用
function calculateTotal(items: Item[]): number {
console.log('Calculating...') // 副作用:I/O操作
analytics.track('total_calculated') // 副作用:修改外部状态
return items.reduce((s, i) => s + i.price, 0)
}将副作用隔离在应用的边缘层:
ts
// 纯业务逻辑
function validateUser(user: User): Result<User, ValidationError> {
// ...
}
// 副作用位于边缘层
async function handleUserCreate(user: User) {
const validation = validateUser(user) // 纯函数
if (!validation.ok) {
logger.warn({ validation }, '无效用户') // 边缘层I/O操作
return
}
await db.user.create(validation.value) // 边缘层I/O操作
}组合小型纯函数:
ts
const normalize = (s: string) => s.trim().toLowerCase()
const validate = (s: string) => s.length > 0
const format = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
function processName(input: string): string | null {
const normalized = normalize(input)
if (!validate(normalized)) return null
return format(normalized)
}When classes ARE OK
派生状态,而非重复存储
| Acceptable | Reason |
|---|---|
Wrapping external SDK ( | Existing API uses class form |
| Long-lived stateful resources (WebSocket handlers) | Lifecycle naturally maps to instance |
Framework requirements (React class components, custom | No alternative |
| Single instance whose constructor does meaningful setup | The class form is genuinely clearer |
For everything else (utility classes, static method collections, data containers, singletons): use a module of functions or a factory.
从源状态计算派生值,不要存储它们。
<good>
interface CartState {
items: readonly CartItem[]
}
function getTotal(state: CartState): number {
return state.items.reduce((sum, item) => sum + item.price, 0)
}
function getItemCount(state: CartState): number {
return state.items.length
}
</good>
<bad>
interface CartState {
items: CartItem[]
total: number // 会与items不同步
itemCount: number // 会与items不同步
}
</bad>Examples
类的适用场景
<example>
<input>"refactor this Counter class to a factory"</input>
<output>
Before:
ts
class Counter {
count = 0
increment() {
this.count++
}
decrement() {
this.count--
}
getValue() {
return this.count
}
}After:
ts
interface Counter {
increment: () => number
decrement: () => number
getValue: () => number
}
export function createCounter(initial: number = 0): Counter {
let value = initial
return {
increment: () => ++value,
decrement: () => --value,
getValue: () => value,
}
}State is now closure-private. Callers don't deal with . Multiple counters are independent without .
</output>
</example>
<example>
<input>"this parseConfig throws — convert to Result"</input>
<output>
thisnewBefore:
ts
function parseConfig(json: string): Config {
if (!json) throw new Error('Empty input')
return JSON.parse(json) // can also throw SyntaxError
}After:
ts
interface ConfigError {
type: 'empty_input' | 'invalid_json'
message: string
}
function parseConfig(json: string): Result<Config, ConfigError> {
if (!json) return err({ type: 'empty_input', message: 'Empty input' })
try {
return ok(JSON.parse(json))
} catch (e) {
return err({ type: 'invalid_json', message: (e as Error).message })
}
}
// caller now must handle both branches at compile time
const result = parseConfig(input)
if (!result.ok) {
return match(result.error)
.with({ type: 'empty_input' }, () => respondWithError(400, 'Empty body'))
.with({ type: 'invalid_json' }, () => respondWithError(400, 'Bad JSON'))
.exhaustive()
}
processConfig(result.value)Errors are part of the type signature now — callers can't accidentally ignore them.
</output>
</example>| 可接受的类使用场景 | 原因 |
|---|---|
封装外部SDK( | 现有API采用类形式 |
| 长期存在的有状态资源(WebSocket处理器) | 生命周期自然映射到实例 |
框架要求(React类组件、自定义 | 无替代方案 |
| 构造函数执行有意义初始化的单例实例 | 类形式确实更清晰 |
除此之外的所有场景(工具类、静态方法集合、数据容器、单例):使用函数模块或工厂函数。
Rationalization table
示例
Captured from RED-baseline transcripts where agents without this skill skipped functional doctrine under pressure. Recognize your own pattern before reaching for the excuse.
| Skipped rule | Verbatim excuse | Why it's wrong |
|---|---|---|
| Replace the class with a factory | "the class works fine and refactoring feels risky — I'll just touch it as little as possible" | The "small change" is exactly when discipline pays off; risk compounds across the next ten changes. The factory is mechanically safe (interface + closure + return), and |
Convert | "Result is ceremony for a 2-line function — try/catch at the caller is fine" | Caller "fine" decays the moment one caller forgets the try/catch. Result puts failure modes into the type signature so the compiler enforces handling. The ceremony is one wrapper. |
Stop mutating | "we own this array, no one else holds a reference — mutation is faster" | Mutations leak through closures, async boundaries, and React renders. "We own it" is true today and false next refactor. Spread/map/filter are O(n) — the same as the loop you just wrote. |
Use | "we've always thrown, the codebase is consistent — switching one function makes it inconsistent" | The codebase is consistently buggy — that is what the rule fixes. Pick a boundary (this module, this PR), apply it consistently inside that boundary, and migrate outward. |
| Pure functions + side effects at the edge | "logging inside the calc is convenient and only one line — pulling it out adds plumbing" | "One line" of side effect makes the function untestable without mocks and unreusable in a different runtime (worker, batch job). Lift the log to the caller; the function stays pure. |
<example>
<input>"refactor this Counter class to a factory"(将这个Counter类重构为工厂函数)</input>
<output>
重构前:
ts
class Counter {
count = 0
increment() {
this.count++
}
decrement() {
this.count--
}
getValue() {
return this.count
}
}重构后:
ts
interface Counter {
increment: () => number
decrement: () => number
getValue: () => number
}
export function createCounter(initial: number = 0): Counter {
let value = initial
return {
increment: () => ++value,
decrement: () => --value,
getValue: () => value,
}
}现在状态通过闭包实现私有。调用方无需处理。无需关键字即可创建多个独立的计数器。
</output>
</example>
<example>
<input>"this parseConfig throws — convert to Result"(这个parseConfig会抛出异常——转为Result实现)</input>
<output>
thisnew修改前:
ts
function parseConfig(json: string): Config {
if (!json) throw new Error('Empty input')
return JSON.parse(json) // 也可能抛出SyntaxError
}修改后:
ts
interface ConfigError {
type: 'empty_input' | 'invalid_json'
message: string
}
function parseConfig(json: string): Result<Config, ConfigError> {
if (!json) return err({ type: 'empty_input', message: 'Empty input' })
try {
return ok(JSON.parse(json))
} catch (e) {
return err({ type: 'invalid_json', message: (e as Error).message })
}
}
// 调用方现在必须在编译时处理两个分支
const result = parseConfig(input)
if (!result.ok) {
return match(result.error)
.with({ type: 'empty_input' }, () => respondWithError(400, 'Empty body'))
.with({ type: 'invalid_json' }, () => respondWithError(400, 'Bad JSON'))
.exhaustive()
}
processConfig(result.value)现在错误成为类型签名的一部分——调用方无法意外忽略错误。
</output>
</example>References
合理化对照表
- ts-pattern — exhaustive matching for handling
Result - type-fest — and other immutability utilities
ReadonlyDeep - Rust's Result type — original inspiration for the pattern
来自RED-baseline对话记录,其中无此Skill的代理会在压力下跳过函数式原则。在找借口之前,请先认清自己的模式。
| 被跳过的规则 | 原话借口 | 错误原因 |
|---|---|---|
| 用工厂函数替代类 | "这个类运行正常,重构有风险——我尽量少改动" | "小改动"正是纪律发挥作用的时候;风险会在接下来的十次改动中累积。工厂函数在机械层面是安全的(接口+闭包+返回),而类容易引发 |
将 | "对于两行函数来说,Result是繁琐的仪式——调用方用try/catch就够了" | 调用方的"够用"会在某个调用方忘记添加try/catch时失效。Result将失败模式纳入类型签名,让编译器强制处理错误。所谓的仪式只是一层包装。 |
停止使用 | "这个数组是我们自己的,没有其他引用——修改更快" | 修改操作会通过闭包、异步边界和React渲染泄漏出去。"我们自己持有"今天成立,下次重构就不成立了。扩展运算符/spread、map/filter的时间复杂度是O(n)——和你刚写的循环一样。 |
解析/验证/I/O操作使用 | "我们一直用抛出异常,代码库保持一致——修改一个函数会破坏一致性" | 代码库是一致的,但也是充满bug的——这正是规则要修复的问题。选择一个边界(如当前模块、当前PR),在边界内一致应用规则,然后向外迁移。 |
| 纯函数+副作用隔离在边缘层 | "在计算函数里加日志很方便,只有一行——把它移出去会增加代码复杂度" | "一行"副作用会让函数无法在不使用mock的情况下测试,也无法在不同运行时(如worker、批处理任务)中复用。将日志移到调用方,函数保持纯函数。 |
—
参考资料
—
- ts-pattern — 用于Result处理的穷举匹配库
- type-fest — 及其他不可变工具类
ReadonlyDeep - Rust的Result type — 该模式的最初灵感来源",