This skill should be used when the user wants to refactor TypeScript code to functional patterns or write new code following functional doctrine. Common triggers include "make this functional", "remove the class", "use Result instead of throw", "stop mutating this", and "refactor to factory function". Bakes in factory functions over classes, Result<T,E> over exceptions, immutable state via spread/map/filter, and pure functions composed in pipelines. Skip when the user wants general TS hygiene (use ts-best-practices), the class wraps a stateful SDK (PrismaClient, Octokit, WebSocket), or a framework requires a class.
Installation
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
This skill should be used when the user wants to refactor TypeScript code to functional patterns or write new code following functional doctrine. Common triggers include "make this functional", "remove the class", "use Result instead of throw", "stop mutating this", and "refactor to factory function". Bakes in factory functions over classes, Result<T,E> over exceptions, immutable state via spread/map/filter, and pure functions composed in pipelines. Skip when the user wants general TS hygiene (use ts-best-practices), the class wraps a stateful SDK (PrismaClient, Octokit, WebSocket), or a framework requires a class.
argument-hint
[<file-or-dir>]
user-invocable
true
model-invocable
true
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.
When to use
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"
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
Core principles
Prefer
Over
Why
Data transformations
Mutations
Predictable, easier to reason about
Functions
Methods
No this binding issues
Composition
Inheritance
Mix behaviors without coupling
Explicit
Implicit
State passed in, not hidden
Factories
Classes
Closure-encapsulated state, no new
Patterns
Factories over classes
Use a factory function returning an interface to encapsulate state. Closures make the state truly private; no this to bind.
interface Counter {
increment: () => number
decrement: () => number
getValue: () => number
}
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.
"this parseConfig throws — convert to Result"
Before:
functionparseConfig(json: string): Config {
if (!json) thrownewError('Empty input')
returnJSON.parse(json) // can also throw SyntaxError
}
After:
interfaceConfigError {
type: 'empty_input' | 'invalid_json'message: string
}
functionparseConfig(json: string): Result<Config, ConfigError> {
if (!json) returnerr({ type: 'empty_input', message: 'Empty input' })
try {
returnok(JSON.parse(json))
} catch (e) {
returnerr({ type: 'invalid_json', message: (e asError).message })
}
}
// caller now must handle both branches at compile timeconst result = parseConfig(input)
if (!result.ok) {
returnmatch(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.
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 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.
References
ts-pattern — exhaustive matching for Result handling
type-fest — ReadonlyDeep and other immutability utilities
Rust's Result type — original inspiration for the pattern