| name | typescript-code-quality |
|---|---|
| description | Use when designing or refactoring code for maintainability, clarity, and strong module boundaries. Emphasize local reasoning, explicit dependencies, separation of side effects from pure logic, separation of control flow from data flow, pragmatic functional patterns over stateful Object Oriented design, and stable module APIs. |
Apply this skill when writing or reviewing code that should be easy to reason about, easy to change, and easy to test.
Local reasoning is the ability to understand what a piece of code does by reading only that piece of code and its immediate inputs/outputs, without tracing through the rest of the codebase.
When you can reason locally, you can change code with confidence. When you can't, every change is a gamble.
A function is easy to reason about locally when:
- Its behavior is fully determined by its arguments
- Everything it produces comes through its return value
- Its type signature is its complete contract (no hidden side-contracts like "must call init() first" or "also updates the cache")
A function is hard to reason about locally when it has implicit dependencies: things it depends on or affects that aren't visible in its signature. Every implicit dependency is a trap for the next person who changes the code. They read the signature, believe they understand the contract, make a reasonable change, and break something because the real contract was wider than what was declared.
-
Shared mutable state. A function reads or writes a variable that other functions also read or write. You can't know what the function will do without knowing the current state, and you can't know what it will break without finding every other reader/writer. This includes global variables, singleton caches, mutable class fields accessed by multiple methods, and module-level state.
-
Action at a distance. Event emitters, pub/sub, observer patterns, middleware chains. A function emits an event; some handler somewhere reacts. The emitter's signature says nothing about the consequences.
-
Implicit ordering constraints. Code that only works if you call
init()beforeprocess(), or setthis.configbefore callingthis.run(). The type system doesn't enforce the order, so correctness lives in the caller's head, not in the code. -
Non-local control flow. Exceptions thrown deep in a call stack and caught somewhere far above. The intermediate layers don't declare that they can fail this way. You can't tell from reading a function whether it might throw or what kind of error.
-
Ambient authority. Functions that reach into the environment: reading env vars, checking the clock, hitting the network, accessing the filesystem. Their behavior changes depending on the world outside the process.
-
Type lies. When a parameter is
stringbut actually must be a specific format, orany/unknownused to sidestep real contracts. The type signature says one thing; the runtime behavior depends on something else.
The distinction:
- Explicit dependency: appears in the function signature (parameters, return type). Visible, compiler-checked.
- Implicit dependency: exists but invisible at the call site. You must read the implementation (or other files) to discover it.
Making dependencies explicit is not about being verbose. It's about making the code honest.
// Explicit: you see exactly what it needs
function getUser(userId: string, db: Database): User;
// Implicit: hides four dependencies behind zero parameters
class UserService {
getUser(): User; // internally uses this.db, this.cache, this.logger, this.metrics
}When you change a function that has only explicit dependencies and no side effects, the blast radius is exactly the set of its callers, which is trivially findable. When you change a function that mutates shared state, emits events, or relies on implicit ordering, the blast radius is unknowable without whole-program analysis.
If a function's entire contract is its type signature, then any change that preserves the signature preserves correctness for all callers. That's a strong, mechanically verifiable guarantee.
Push I/O, network calls, filesystem access, DB writes, logging, and framework integration to the edges.
Prefer:
- Pure functions for parsing, validation, mapping, planning, normalization, and decision-making
- Thin effectful wrappers that call pure logic and then perform the side effect
Good shape:
- Read inputs
- Build a pure plan
- Execute the plan
Avoid:
- Mixing fetch/write/log calls into transformation logic
- Hidden mutation inside utility functions
- Business logic embedded in framework handlers
Keep orchestration code distinct from transformation code.
Prefer:
- Orchestrators that answer: "In what order do steps happen?"
- Pure helpers that answer: "How is data transformed?"
- Data structures that make decisions explicit
Good shape:
buildPlan(input) -> PlanpartitionPlan(plan) -> { ... }executePlan(plan) -> Result
Avoid:
- Branch-heavy functions that both decide and mutate
- Recomputing the same derived state in multiple places
- Encoding important logic implicitly in string manipulation or ad hoc conditionals
Default to functions and data structures unless objects clearly reduce complexity.
Prefer:
- Plain functions
- Immutable data flow
- Tagged unions / discriminated unions
- Explicit inputs and outputs
- Small composable helpers
- No shared or global mutation
Use objects/classes only when they model real lifecycle or stateful resources cleanly.
Avoid:
- Classes that mostly act as namespaces
- Stateful helpers with hidden internal behavior
- Methods that depend on initialization order or mutation history
forloops that accumulate into arrays or objects viapushor reassignment whenmap,filter,flatMap,reduce, or object spread would express the transformation more directly
Each module should have one job and expose a small surface area.
Prefer modules like:
planner: derive normalized plans from raw inputsexecutor: perform effectful work from plansformatter: transform data for presentationrepository: read/write external systems
Module boundaries should make it obvious:
- what goes in
- what comes out
- what side effects happen
- what invariants are guaranteed
Avoid:
- "utils" modules with mixed responsibilities
- leaking transport/framework types across layers
- requiring callers to understand internal representation details
When the system has multiple execution modes, represent them directly.
Prefer:
type ToolPlan =
| { kind: "mcp"; ... }
| { kind: "legacy"; ... };When transforming one collection into another, prefer declarative operators over manual accumulation.
Prefer:
const activeNames = users
.filter((user) => user.isActive)
.map((user) => user.name);
const allCommands = groups.flatMap((group) => group.commands);Avoid:
const activeNames: string[] = [];
for (const user of users) {
if (user.isActive) activeNames.push(user.name);
}The declarative version makes the intent obvious: filter, then map. The mutating version makes the reader track control flow and intermediate state. That cost matters even when the mutation is technically local.
Use local mutation only when there is a clear payoff, such as a measured performance need, an algorithm that is genuinely clearer imperatively, or an API that is inherently mutating. In those cases, isolate the mutation tightly and do not let it leak into surrounding design.