ts-best-practices-functional

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ts-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
    ,
    Octokit
    , WebSocket connections) — keep it
  • 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

不适用场景

PreferOverWhy
Data transformationsMutationsPredictable, easier to reason about
FunctionsMethodsNo
this
binding issues
CompositionInheritanceMix behaviors without coupling
ExplicitImplicitState passed in, not hidden
FactoriesClassesClosure-encapsulated state, no
new
  • 类用于封装有状态的外部SDK(
    PrismaClient
    Octokit
    、WebSocket连接)——保留类
  • 框架要求必须使用类(传统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
this
to bind.
<good> interface Counter { increment: () => number decrement: () => number getValue: () => number }
function 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 —
this
is lost! </bad>
优先选择而非原因
数据转换修改操作可预测性更强,更易理解
函数方法
this
绑定问题
组合模式继承无需耦合即可混合行为
显式实现隐式实现状态传入而非隐藏
工厂函数通过闭包封装状态,无需
new
关键字

Factory advantages

模式

优先使用工厂函数而非类

  • No
    this
    confusion
  • No
    new
    keyword
  • 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) }
}
使用返回接口的工厂函数封装状态。闭包可实现真正的私有状态;无需绑定
this
<good> interface Counter { increment: () => number decrement: () => number getValue: () => number }
function 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 —
this
丢失! </bad>

Immutability 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
readonly
modifiers and
as const
to enforce at the type level:
ts
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
Result<T, E>
instead of throwing. Errors become part of the type signature.
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 })
<good> interface ParseError { type: 'invalid_json' | 'schema_mismatch' message: string }
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>
使用
readonly
修饰符和
as const
在类型层面强制执行不可变:
ts
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 ResultDon't use Result
JSON parsing, validationTruly exceptional errors (out-of-memory)
External API callsProgramming bugs (assertion failures)
File I/O, networkInternal invariants that should never fail
Business logic with known failure modesOperations 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 })
<good> interface ParseError { type: 'invalid_json' | 'schema_mismatch' message: string }
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
Error
loses information.
ts
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.value

Pure 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)
}
为每个领域定义错误类型——通用
Error
会丢失信息。
ts
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

派生状态,而非重复存储

AcceptableReason
Wrapping external SDK (
PrismaClient
,
Octokit
)
Existing API uses class form
Long-lived stateful resources (WebSocket handlers)Lifecycle naturally maps to instance
Framework requirements (React class components, custom
Error
)
No alternative
Single instance whose constructor does meaningful setupThe 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
this
. Multiple counters are independent without
new
.
</output> </example> <example> <input>"this parseConfig throws — convert to Result"</input> <output>
Before:
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(
PrismaClient
Octokit
现有API采用类形式
长期存在的有状态资源(WebSocket处理器)生命周期自然映射到实例
框架要求(React类组件、自定义
Error
无替代方案
构造函数执行有意义初始化的单例实例类形式确实更清晰
除此之外的所有场景(工具类、静态方法集合、数据容器、单例):使用函数模块或工厂函数。

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 ruleVerbatim excuseWhy 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
this
-binding bugs are a real prod cost the class invites.
Convert
throw new Error
to
Result<T,E>
"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
items.push
/
Object.assign
"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
Result
for parse / validate / I/O
"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,
  }
}
现在状态通过闭包实现私有。调用方无需处理
this
。无需
new
关键字即可创建多个独立的计数器。
</output> </example> <example> <input>"this parseConfig throws — convert to Result"(这个parseConfig会抛出异常——转为Result实现)</input> <output>
修改前:
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
    Result
    handling
  • type-fest
    ReadonlyDeep
    and other immutability utilities
  • Rust's Result type — original inspiration for the pattern
来自RED-baseline对话记录,其中无此Skill的代理会在压力下跳过函数式原则。在找借口之前,请先认清自己的模式。
被跳过的规则原话借口错误原因
用工厂函数替代类"这个类运行正常,重构有风险——我尽量少改动""小改动"正是纪律发挥作用的时候;风险会在接下来的十次改动中累积。工厂函数在机械层面是安全的(接口+闭包+返回),而类容易引发
this
绑定错误,这是真实的生产环境成本。
throw new Error
转为
Result<T,E>
"对于两行函数来说,Result是繁琐的仪式——调用方用try/catch就够了"调用方的"够用"会在某个调用方忘记添加try/catch时失效。Result将失败模式纳入类型签名,让编译器强制处理错误。所谓的仪式只是一层包装。
停止使用
items.push
/
Object.assign
修改对象
"这个数组是我们自己的,没有其他引用——修改更快"修改操作会通过闭包、异步边界和React渲染泄漏出去。"我们自己持有"今天成立,下次重构就不成立了。扩展运算符/spread、map/filter的时间复杂度是O(n)——和你刚写的循环一样。
解析/验证/I/O操作使用
Result
"我们一直用抛出异常,代码库保持一致——修改一个函数会破坏一致性"代码库是一致的,但也是充满bug的——这正是规则要修复的问题。选择一个边界(如当前模块、当前PR),在边界内一致应用规则,然后向外迁移。
纯函数+副作用隔离在边缘层"在计算函数里加日志很方便,只有一行——把它移出去会增加代码复杂度""一行"副作用会让函数无法在不使用mock的情况下测试,也无法在不同运行时(如worker、批处理任务)中复用。将日志移到调用方,函数保持纯函数。

参考资料

  • ts-pattern — 用于Result处理的穷举匹配库
  • type-fest
    ReadonlyDeep
    及其他不可变工具类
  • Rust的Result type — 该模式的最初灵感来源",