atelier-typescript-functional-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFunctional Patterns for Reliable TypeScript
可靠TypeScript的函数式编程模式
Build reliable systems using Algebraic Data Types (ADTs), discriminated unions, Result/Option types, and branded types. These patterns enable the compiler to prove correctness, prevent runtime errors, and make illegal states unrepresentable.
使用代数数据类型(ADTs)、可辨识联合、Result/Option类型和品牌化类型构建可靠系统。这些模式让编译器能够验证正确性,防止运行时错误,并使非法状态无法被表示。
Why Functional Patterns?
为什么选择函数式编程模式?
Reliability through types: Use the type system to encode business rules, making invalid states impossible to construct. The compiler becomes your safety net, catching errors at build time rather than runtime.
Key benefits:
- Exhaustiveness checking prevents missing cases
- Impossible states become unrepresentable
- Business logic encoded in types, not runtime checks
- Refactoring becomes safe and mechanical
- Self-documenting code through types
通过类型保障可靠性:利用类型系统编码业务规则,使无效状态无法被构造。编译器成为你的安全网,在构建阶段而非运行时捕获错误。
核心优势:
- 穷尽性检查防止遗漏场景
- 非法状态无法被表示
- 业务规则编码在类型中,而非运行时检查
- 重构变得安全且可机械化
- 通过类型实现自文档化代码
Quick Reference
快速参考
For detailed patterns and examples, see:
- ADTs (Algebraic Data Types) - Sum types, product types, discriminated unions
- Option & Result - Type-safe error handling and nullable values
- Branded Types - Smart constructors and nominal typing
- Migration Guide - Step-by-step adoption playbook
如需详细模式和示例,请查看:
- ADTs (Algebraic Data Types) - 和类型、积类型、可辨识联合
- Option & Result - 类型安全的错误处理和可空值
- Branded Types - 智能构造函数和标称类型
- Migration Guide - 分步采用指南
Core Patterns Overview
核心模式概述
1. Discriminated Unions (Sum Types)
1. 可辨识联合(和类型)
Model "one of several variants" with exhaustive pattern matching:
typescript
type PaymentMethod =
| { kind: "card"; last4: string; brand: string }
| { kind: "ach"; accountNumber: string; routingNumber: string }
| { kind: "wallet"; provider: "apple" | "google" }
function processPayment(method: PaymentMethod): void {
switch (method.kind) {
case "card":
// TypeScript knows: method.last4 and method.brand exist
return processCard(method.last4, method.brand)
case "ach":
// TypeScript knows: method.accountNumber and method.routingNumber exist
return processACH(method.accountNumber, method.routingNumber)
case "wallet":
// TypeScript knows: method.provider exists
return processWallet(method.provider)
default:
assertNever(method) // Compiler error if cases missing
}
}通过穷尽模式匹配建模“多个变体之一”:
typescript
type PaymentMethod =
| { kind: "card"; last4: string; brand: string }
| { kind: "ach"; accountNumber: string; routingNumber: string }
| { kind: "wallet"; provider: "apple" | "google" }
function processPayment(method: PaymentMethod): void {
switch (method.kind) {
case "card":
// TypeScript knows: method.last4 and method.brand exist
return processCard(method.last4, method.brand)
case "ach":
// TypeScript knows: method.accountNumber and method.routingNumber exist
return processACH(method.accountNumber, method.routingNumber)
case "wallet":
// TypeScript knows: method.provider exists
return processWallet(method.provider)
default:
assertNever(method) // Compiler error if cases missing
}
}2. Option Type (Nullable Values)
2. Option类型(可空值)
Explicit handling of "value may be absent":
typescript
type Option<T> = { _tag: "None" } | { _tag: "Some"; value: T }
function findUser(id: string): Option<User> {
const user = database.get(id)
return user ? Some(user) : None
}
const result = findUser("123")
switch (result._tag) {
case "Some":
console.log(result.value.name) // Type-safe access
break
case "None":
console.log("User not found")
break
}显式处理“值可能不存在”的场景:
typescript
type Option<T> = { _tag: "None" } | { _tag: "Some"; value: T }
function findUser(id: string): Option<User> {
const user = database.get(id)
return user ? Some(user) : None
}
const result = findUser("123")
switch (result._tag) {
case "Some":
console.log(result.value.name) // Type-safe access
break
case "None":
console.log("User not found")
break
}3. Result Type (Error Handling)
3. Result类型(错误处理)
Explicit error handling without exceptions:
typescript
type Result<T, E> = { _tag: "Ok"; value: T } | { _tag: "Err"; error: E }
function parseConfig(raw: string): Result<Config, ParseError> {
try {
const data = JSON.parse(raw)
return Ok(validateConfig(data))
} catch (e) {
return Err({ message: "Invalid JSON", cause: e })
}
}
const result = parseConfig(rawConfig)
switch (result._tag) {
case "Ok":
startServer(result.value)
break
case "Err":
logger.error(result.error.message)
break
}无需异常的显式错误处理:
typescript
type Result<T, E> = { _tag: "Ok"; value: T } | { _tag: "Err"; error: E }
function parseConfig(raw: string): Result<Config, ParseError> {
try {
const data = JSON.parse(raw)
return Ok(validateConfig(data))
} catch (e) {
return Err({ message: "Invalid JSON", cause: e })
}
}
const result = parseConfig(rawConfig)
switch (result._tag) {
case "Ok":
startServer(result.value)
break
case "Err":
logger.error(result.error.message)
break
}4. Branded Types (Type-Safe Units)
4. 品牌化类型(类型安全单位)
Prevent unit confusion and invalid values:
typescript
type Brand<K, T> = K & { __brand: T }
type Cents = Brand<number, "Cents">
type Dollars = Brand<number, "Dollars">
const Cents = (n: number): Cents => {
if (!Number.isInteger(n) || n < 0) throw new Error("Invalid cents")
return n as Cents
}
const Dollars = (n: number): Dollars => {
if (n < 0) throw new Error("Invalid dollars")
return n as Dollars
}
// Compiler prevents mixing units:
const price: Cents = Cents(100)
const budget: Dollars = Dollars(10)
const total: Cents = price + budget // Type error! Cannot mix Cents and Dollars防止单位混淆和无效值:
typescript
type Brand<K, T> = K & { __brand: T }
type Cents = Brand<number, "Cents">
type Dollars = Brand<number, "Dollars">
const Cents = (n: number): Cents => {
if (!Number.isInteger(n) || n < 0) throw new Error("Invalid cents")
return n as Cents
}
const Dollars = (n: number): Dollars => {
if (n < 0) throw new Error("Invalid dollars")
return n as Dollars
}
// Compiler prevents mixing units:
const price: Cents = Cents(100)
const budget: Dollars = Dollars(10)
const total: Cents = price + budget // Type error! Cannot mix Cents and DollarsWhen to Use
使用场景
Use Discriminated Unions When:
何时使用可辨识联合:
- Modeling state machines (pending → settled → reconciled)
- Representing mutually exclusive variants (payment methods, user roles)
- Building domain models with distinct states
- Replacing boolean flags with explicit states
- 建模状态机(pending → settled → reconciled)
- 表示互斥变体(支付方式、用户角色)
- 构建具有不同状态的领域模型
- 用显式状态替代布尔标志
Use Option When:
何时使用Option:
- Value may be absent (but absence is expected/valid)
- Replacing or
nullchecksundefined - Chaining operations that may fail to find values
- Making nullability explicit in APIs
- 值可能不存在(但缺失是预期/有效的)
- 替代或
null检查undefined - 链式调用可能无法找到值的操作
- 在API中显式声明可空性
Use Result When:
何时使用Result:
- Operation may fail with recoverable errors
- You need to propagate error context
- Replacing try/catch for expected failures
- Building error handling into function signatures
- 操作可能因可恢复错误失败
- 需要传播错误上下文
- 替代try/catch处理预期失败
- 在函数签名中内置错误处理
Use Branded Types When:
何时使用品牌化类型:
- Preventing unit confusion (cents vs dollars, ms vs seconds)
- Enforcing validation invariants (email format, positive numbers)
- Creating type-safe IDs (UserId vs OrderId)
- Domain-driven design with value objects
- 防止单位混淆(美分与美元、毫秒与秒)
- 强制执行验证不变量(邮箱格式、正数)
- 创建类型安全的ID(UserId vs OrderId)
- 领域驱动设计中的值对象
Quick Start - Paste-Ready Helpers
快速开始 - 可直接复制的工具函数
Copy these into your project to start using functional patterns:
typescript
// ============================================
// Option Type
// ============================================
type None = { _tag: "None" }
type Some<T> = { _tag: "Some"; value: T }
type Option<T> = None | Some<T>
const None: None = { _tag: "None" }
const Some = <T>(value: T): Option<T> => ({ _tag: "Some", value })
// Utilities
const isNone = <T>(opt: Option<T>): opt is None => opt._tag === "None"
const isSome = <T>(opt: Option<T>): opt is Some<T> => opt._tag === "Some"
const getOrElse = <T>(opt: Option<T>, defaultValue: T): T =>
opt._tag === "Some" ? opt.value : defaultValue
const map = <T, U>(opt: Option<T>, fn: (value: T) => U): Option<U> =>
opt._tag === "Some" ? Some(fn(opt.value)) : None
const flatMap = <T, U>(opt: Option<T>, fn: (value: T) => Option<U>): Option<U> =>
opt._tag === "Some" ? fn(opt.value) : None
// ============================================
// Result Type
// ============================================
type Ok<T> = { _tag: "Ok"; value: T }
type Err<E> = { _tag: "Err"; error: E }
type Result<T, E> = Ok<T> | Err<E>
const Ok = <T>(value: T): Result<T, never> => ({ _tag: "Ok", value })
const Err = <E>(error: E): Result<never, E> => ({ _tag: "Err", error })
// Utilities
const isOk = <T, E>(result: Result<T, E>): result is Ok<T> => result._tag === "Ok"
const isErr = <T, E>(result: Result<T, E>): result is Err<E> => result._tag === "Err"
const mapResult = <T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> =>
result._tag === "Ok" ? Ok(fn(result.value)) : result
const flatMapResult = <T, U, E>(
result: Result<T, E>,
fn: (value: T) => Result<U, E>
): Result<U, E> =>
result._tag === "Ok" ? fn(result.value) : result
// ============================================
// Exhaustiveness Checking
// ============================================
const assertNever = (x: never): never => {
throw new Error(`Unhandled variant: ${JSON.stringify(x)}`)
}
// ============================================
// Branded Types
// ============================================
type Brand<K, T> = K & { __brand: T }
// Example: Cents (integer cents to prevent floating point errors)
type Cents = Brand<number, "Cents">
const Cents = (n: number): Cents => {
if (!Number.isInteger(n)) throw new Error("Cents must be integer")
if (n < 0) throw new Error("Cents cannot be negative")
return n as Cents
}
// Example: Email (validated email address)
type Email = Brand<string, "Email">
const Email = (s: string): Email => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)) throw new Error("Invalid email")
return s as Email
}
// Example: Millis (timestamp in milliseconds)
type Millis = Brand<number, "Millis">
const Millis = (n: number): Millis => {
if (n < 0) throw new Error("Millis cannot be negative")
return n as Millis
}将以下代码复制到你的项目中,即可开始使用函数式模式:
typescript
// ============================================
// Option Type
// ============================================
type None = { _tag: "None" }
type Some<T> = { _tag: "Some"; value: T }
type Option<T> = None | Some<T>
const None: None = { _tag: "None" }
const Some = <T>(value: T): Option<T> => ({ _tag: "Some", value })
// Utilities
const isNone = <T>(opt: Option<T>): opt is None => opt._tag === "None"
const isSome = <T>(opt: Option<T>): opt is Some<T> => opt._tag === "Some"
const getOrElse = <T>(opt: Option<T>, defaultValue: T): T =>
opt._tag === "Some" ? opt.value : defaultValue
const map = <T, U>(opt: Option<T>, fn: (value: T) => U): Option<U> =>
opt._tag === "Some" ? Some(fn(opt.value)) : None
const flatMap = <T, U>(opt: Option<T>, fn: (value: T) => Option<U>): Option<U> =>
opt._tag === "Some" ? fn(opt.value) : None
// ============================================
// Result Type
// ============================================
type Ok<T> = { _tag: "Ok"; value: T }
type Err<E> = { _tag: "Err"; error: E }
type Result<T, E> = Ok<T> | Err<E>
const Ok = <T>(value: T): Result<T, never> => ({ _tag: "Ok", value })
const Err = <E>(error: E): Result<never, E> => ({ _tag: "Err", error })
// Utilities
const isOk = <T, E>(result: Result<T, E>): result is Ok<T> => result._tag === "Ok"
const isErr = <T, E>(result: Result<T, E>): result is Err<E> => result._tag === "Err"
const mapResult = <T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> =>
result._tag === "Ok" ? Ok(fn(result.value)) : result
const flatMapResult = <T, U, E>(
result: Result<T, E>,
fn: (value: T) => Result<U, E>
): Result<U, E> =>
result._tag === "Ok" ? fn(result.value) : result
// ============================================
// Exhaustiveness Checking
// ============================================
const assertNever = (x: never): never => {
throw new Error(`Unhandled variant: ${JSON.stringify(x)}`)
}
// ============================================
// Branded Types
// ============================================
type Brand<K, T> = K & { __brand: T }
// Example: Cents (integer cents to prevent floating point errors)
type Cents = Brand<number, "Cents">
const Cents = (n: number): Cents => {
if (!Number.isInteger(n)) throw new Error("Cents must be integer")
if (n < 0) throw new Error("Cents cannot be negative")
return n as Cents
}
// Example: Email (validated email address)
type Email = Brand<string, "Email">
const Email = (s: string): Email => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)) throw new Error("Invalid email")
return s as Email
}
// Example: Millis (timestamp in milliseconds)
type Millis = Brand<number, "Millis">
const Millis = (n: number): Millis => {
if (n < 0) throw new Error("Millis cannot be negative")
return n as Millis
}Guidelines
指南
Pattern Matching Best Practices
模式匹配最佳实践
-
Always usein default case for exhaustiveness checking:
assertNevertypescriptswitch (variant.kind) { case "a": return handleA(variant) case "b": return handleB(variant) default: assertNever(variant) // Compiler error if cases missing } -
Use discriminant field consistently (,
kind,type):_tagtypescript// Good: consistent discriminant type Result<T, E> = { _tag: "Ok"; value: T } | { _tag: "Err"; error: E } // Avoid: mixing discriminants type Bad = { kind: "a" } | { type: "b" } // Inconsistent! -
Narrow types early to unlock type safety:typescript
if (result._tag === "Ok") { // TypeScript knows: result.value exists return result.value.data }
-
**始终在default分支使用**进行穷尽性检查:
assertNevertypescriptswitch (variant.kind) { case "a": return handleA(variant) case "b": return handleB(variant) default: assertNever(variant) // Compiler error if cases missing } -
一致使用判别字段(、
kind、type):_tagtypescript// Good: consistent discriminant type Result<T, E> = { _tag: "Ok"; value: T } | { _tag: "Err"; error: E } // Avoid: mixing discriminants type Bad = { kind: "a" } | { type: "b" } // Inconsistent! -
尽早收窄类型以解锁类型安全:typescript
if (result._tag === "Ok") { // TypeScript knows: result.value exists return result.value.data }
Error Handling Strategy
错误处理策略
-
Use Option for expected absence:typescript
function findUser(id: string): Option<User> -
Use Result for recoverable errors:typescript
function parseConfig(raw: string): Result<Config, ParseError> -
Use exceptions for programmer errors:typescript
function unreachable(message: string): never { throw new Error(`Unreachable: ${message}`) }
-
使用Option处理预期缺失:typescript
function findUser(id: string): Option<User> -
使用Result处理可恢复错误:typescript
function parseConfig(raw: string): Result<Config, ParseError> -
使用异常处理程序员错误:typescript
function unreachable(message: string): never { throw new Error(`Unreachable: ${message}`) }
Branded Types Guidelines
品牌化类型指南
-
Validate in smart constructor:typescript
const PositiveInt = (n: number): PositiveInt => { if (!Number.isInteger(n) || n <= 0) throw new Error("Must be positive integer") return n as PositiveInt } -
Use branded types for domain concepts:typescript
type UserId = Brand<string, "UserId"> type OrderId = Brand<string, "OrderId"> // Compiler prevents: const userId: UserId = orderId -
Prevent unit confusion:typescript
type Seconds = Brand<number, "Seconds"> type Millis = Brand<number, "Millis"> // Compiler prevents: const s: Seconds = millis
-
在智能构造函数中验证:typescript
const PositiveInt = (n: number): PositiveInt => { if (!Number.isInteger(n) || n <= 0) throw new Error("Must be positive integer") return n as PositiveInt } -
为领域概念使用品牌化类型:typescript
type UserId = Brand<string, "UserId"> type OrderId = Brand<string, "OrderId"> // Compiler prevents: const userId: UserId = orderId -
防止单位混淆:typescript
type Seconds = Brand<number, "Seconds"> type Millis = Brand<number, "Millis"> // Compiler prevents: const s: Seconds = millis
Migration Strategy
迁移策略
Start small and expand:
- New features: Use functional patterns from day one
- Bug fixes: Refactor to discriminated unions when touching code
- High-risk areas: Prioritize financial calculations, state machines
- Team adoption: Share paste-ready helpers, pair on first implementations
Enable TypeScript strict mode flags:
- - Make nullability explicit
strictNullChecks: true - - Ensure all code paths return
noImplicitReturns: true - - Safer function signatures
strictFunctionTypes: true
从小规模开始,逐步扩展:
- 新功能:从第一天起使用函数式模式
- Bug修复:修改代码时重构为可辨识联合
- 高风险领域:优先处理财务计算、状态机
- 团队采用:分享可直接复制的工具函数,结对完成首次实现
启用TypeScript严格模式标志:
- - 使可空性显式
strictNullChecks: true - - 确保所有代码路径都有返回值
noImplicitReturns: true - - 更安全的函数签名
strictFunctionTypes: true
Examples by Domain
领域示例
State Machine (Transaction Lifecycle)
状态机(交易生命周期)
typescript
type TxnState =
| { kind: "pending"; createdAt: Millis }
| { kind: "settled"; ledgerId: string; settledAt: Millis }
| { kind: "failed"; reason: FailureReason; failedAt: Millis }
| { kind: "reversed"; originalLedgerId: string; reversedAt: Millis }
function canReverse(state: TxnState): boolean {
switch (state.kind) {
case "pending": return false
case "settled": return true
case "failed": return false
case "reversed": return false
default: assertNever(state)
}
}typescript
type TxnState =
| { kind: "pending"; createdAt: Millis }
| { kind: "settled"; ledgerId: string; settledAt: Millis }
| { kind: "failed"; reason: FailureReason; failedAt: Millis }
| { kind: "reversed"; originalLedgerId: string; reversedAt: Millis }
function canReverse(state: TxnState): boolean {
switch (state.kind) {
case "pending": return false
case "settled": return true
case "failed": return false
case "reversed": return false
default: assertNever(state)
}
}Configuration Parsing
配置解析
typescript
type ConfigError = { field: string; message: string }
function parsePort(raw: unknown): Result<number, ConfigError> {
if (typeof raw !== "number") {
return Err({ field: "port", message: "must be number" })
}
if (raw < 1 || raw > 65535) {
return Err({ field: "port", message: "must be 1-65535" })
}
return Ok(raw)
}typescript
type ConfigError = { field: string; message: string }
function parsePort(raw: unknown): Result<number, ConfigError> {
if (typeof raw !== "number") {
return Err({ field: "port", message: "must be number" })
}
if (raw < 1 || raw > 65535) {
return Err({ field: "port", message: "must be 1-65535" })
}
return Ok(raw)
}Financial Calculations
财务计算
typescript
type Cents = Brand<number, "Cents">
function addCents(a: Cents, b: Cents): Cents {
return Cents(a + b) // Smart constructor validates result
}
function calculateFee(amount: Cents, bps: number): Cents {
const feeAmount = Math.round((amount * bps) / 10000)
return Cents(feeAmount)
}typescript
type Cents = Brand<number, "Cents">
function addCents(a: Cents, b: Cents): Cents {
return Cents(a + b) // Smart constructor validates result
}
function calculateFee(amount: Cents, bps: number): Cents {
const feeAmount = Math.round((amount * bps) / 10000)
return Cents(feeAmount)
}Further Reading
延伸阅读
- ADT Reference - Deep dive on sum types, product types, and pattern matching
- Option & Result Reference - Comprehensive error handling patterns
- Branded Types Reference - Advanced nominal typing techniques
- Migration Guide - Step-by-step adoption playbook
- ADT Reference - 和类型、积类型、模式匹配深度解析
- Option & Result Reference - 全面的错误处理模式
- Branded Types Reference - 高级标称类型技巧
- Migration Guide - 分步采用指南
Credits
致谢
These patterns are inspired by Why Reliability Demands Functional Programming, ADTs, Safety and Critical Infrastructure by Rastrian. The blog post explores how functional programming techniques and Algebraic Data Types enable building reliable systems in critical infrastructure contexts.
这些模式灵感来源于Rastrian的文章**Why Reliability Demands Functional Programming, ADTs, Safety and Critical Infrastructure**。该博文探讨了函数式编程技术和代数数据类型如何在关键基础设施场景中构建可靠系统。
When This Skill Loads
本技能加载时机
This skill automatically loads when discussing:
- Discriminated unions and sum types
- State machine modeling
- Result/Option types and error handling
- Branded types and smart constructors
- Type-safe domain models
- Making illegal states unrepresentable
- Functional programming in TypeScript
当讨论以下内容时,本技能会自动加载:
- 可辨识联合与和类型
- 状态机建模
- Result/Option类型与错误处理
- 品牌化类型与智能构造函数
- 类型安全领域模型
- 使非法状态无法被表示
- TypeScript中的函数式编程