Loading...
Loading...
Functional programming patterns for reliable TypeScript. Use when modeling state machines, discriminated unions, Result/Option types, branded types, or building type-safe domain models.
npx skill4agent add martinffx/claude-code-atelier atelier-typescript-functional-patternstype 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
}
}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
}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
}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 Dollarsnullundefined// ============================================
// 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
}assertNeverswitch (variant.kind) {
case "a": return handleA(variant)
case "b": return handleB(variant)
default: assertNever(variant) // Compiler error if cases missing
}kindtype_tag// 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!if (result._tag === "Ok") {
// TypeScript knows: result.value exists
return result.value.data
}function findUser(id: string): Option<User>function parseConfig(raw: string): Result<Config, ParseError>function unreachable(message: string): never {
throw new Error(`Unreachable: ${message}`)
}const PositiveInt = (n: number): PositiveInt => {
if (!Number.isInteger(n) || n <= 0) throw new Error("Must be positive integer")
return n as PositiveInt
}type UserId = Brand<string, "UserId">
type OrderId = Brand<string, "OrderId">
// Compiler prevents: const userId: UserId = orderIdtype Seconds = Brand<number, "Seconds">
type Millis = Brand<number, "Millis">
// Compiler prevents: const s: Seconds = millisstrictNullChecks: truenoImplicitReturns: truestrictFunctionTypes: truetype 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)
}
}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)
}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)
}