Expressive TypeScript
Quick Guide: Write code that communicates its intent without requiring the reader to mentally simulate any of its parts. Apply the two-tier pattern: orchestrators at the top that read like pseudocode, pure functions at the bottom that each do one thing. Extract until the code reads like prose. Use utility libraries only when they genuinely improve readability over plain JS.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST structure every non-trivial function as a two-tier orchestrator: guard clauses at the top, named function calls in the middle, assembly at the bottom -- NO inline data transformations in orchestrators)
(You MUST extract any expression that requires mental simulation to understand into a named function or named constant)
(You MUST name functions for WHAT they do, not HOW they do it -- isContentAddition(line) not checkLineStartsWithPlusButNotTriplePlus(line))
(You MUST read the existing code before refactoring -- understand the current structure, then improve it)
(You MUST prefer plain JS methods (.map(), .filter(), .reduce()) over utility libraries when they already read clearly)
</critical_requirements>
Auto-detection: orchestrator pattern, two-tier function, extract function, named predicate, named constant, readability refactor, expressive code, function decomposition, pure function extraction, readable TypeScript, guard clause, early return, flatten conditionals, discriminated union, exhaustive switch, as const satisfies, async orchestrator
When to use:
- Writing any function that mixes validation, transformation, and assembly logic
- Refactoring a function where you need to mentally simulate steps to understand the flow
- Naming predicates, constants, or transforms to communicate intent
- Deciding whether to use a utility library function or plain JS
- Decomposing a large function into orchestrator + pure helpers
- Reviewing code and finding blocks that require mental simulation
- Flattening deeply nested if/else blocks into guard clauses
- Writing async functions that orchestrate multiple independent operations
- Modeling state or events with discriminated unions for exhaustive handling
When NOT to use:
- Writing simple one-liner functions that are already clear
- Academic functional programming (monads, functors, Either/Option types)
- Point-free style where arguments are implicit
- Over-extracting trivially simple expressions into named functions
- Performance-critical hot paths where function call overhead matters
Key patterns covered:
- The two-tier pattern (orchestrator + pure functions)
- The readability test ("can you understand without simulating?")
- Named predicates, constants, and transforms
- Guard clauses: flattening nested conditionals with early returns
- Discriminated unions + exhaustive switch for type-safe control flow
- Async orchestrators with
Promise.allfor independent operations as const satisfiesfor intent-revealing configuration- The extraction decision framework
- Utility library usage: the 80/20 rule
Detailed Resources
- examples/core.md - Two-tier pattern, guard clauses, named predicates, named constants, discriminated unions, async orchestrators
- examples/data-transforms.md - Data transformation patterns, when plain JS is enough, when utility libraries help
- reference.md - Quick-reference cheat sheet with decision tables
<philosophy>
Philosophy
Expressive TypeScript is practical, 80/20 functional programming focused on readability. The core test for any block of code:
"Can someone understand the code's flow without simulating any of its parts?"
If the answer is no, extract the part that requires simulation into a named function or constant. If the answer is yes, leave it alone -- even if it could theoretically be "cleaner."
This is NOT:
- Monads, functors, or Either/Option types -- those belong in a different skill
- Point-free style -- implicit arguments obscure intent for most readers
- Religious functional purity -- side effects in orchestrators are fine; the pure functions underneath are what matter
- Over-extraction -- three similar lines of code is better than a premature abstraction
The two core ideas:
- Orchestrators read like pseudocode. Guard clauses, named function calls, assembly. No inline logic that requires simulation.
- Pure functions do one thing. Each has a name that communicates its purpose. Each is independently testable.
When to apply this skill:
- Any function longer than ~15 lines that mixes concerns
- Any expression where a reader would need to trace through logic to understand intent
- Any repeated logic pattern that lacks a descriptive name
When NOT to apply:
- A single
.map()or.filter()that already reads clearly - Functions that are already one level of abstraction
- Trivially simple code where extraction would add noise
<patterns>
Core Patterns
Pattern 1: The Two-Tier Pattern
Every non-trivial function follows the same structure: an orchestrator at the top that reads like pseudocode, calling pure functions at the bottom that each do one thing.
The Orchestrator (Top Tier)
// Orchestrator: reads like a step-by-step plan
function processUserImport(rawData: RawImportData): ImportResult {
// 1. Guard clauses
if (!rawData.users.length) {
return { imported: 0, skipped: 0, errors: [] };
}
// 2. Named function calls for each step
const validated = rawData.users.filter(isValidUser);
const normalized = validated.map(normalizeUserRecord);
const deduped = removeDuplicatesByEmail(normalized);
const { existing, newUsers } = separateExistingUsers(deduped);
// 3. Assembly
return {
imported: newUsers.length,
skipped: existing.length,
errors: rawData.users.length - validated.length,
};
}
Why good: Each line communicates intent through its function name. A reader knows WHAT happens at each step without reading HOW any step works. Guard clauses are at the top. No inline lambdas obscure the flow.
The Pure Functions (Bottom Tier)
// Pure function: one job, named for purpose
function isValidUser(user: RawUser): boolean {
return user.email.includes("@") && user.name.trim().length > 0;
}
function normalizeUserRecord(user: RawUser): NormalizedUser {
return {
email: user.email.toLowerCase().trim(),
name: user.name.trim(),
role: user.role ?? DEFAULT_ROLE,
};
}
function removeDuplicatesByEmail(users: NormalizedUser[]): NormalizedUser[] {
const seen = new Set<string>();
return users.filter((user) => {
if (seen.has(user.email)) return false;
seen.add(user.email);
return true;
});
}
Why good: Each function does one thing, has a descriptive name, and is testable in isolation without mocks or state setup.
Full before/after examples: See examples/core.md for complete orchestrator transformations.
Pattern 2: The Readability Test
Before extracting or refactoring, apply this test to any block of code:
"Can someone understand the code's flow without mentally simulating any of its parts?"
// Fails the readability test -- requires simulation
const result = items
.filter(
(item) =>
item.status === "active" &&
item.createdAt > cutoffDate &&
!excludedIds.has(item.id),
)
.map((item) => ({
...item,
displayName: item.firstName + " " + item.lastName,
age: Math.floor((Date.now() - item.birthDate.getTime()) / MS_PER_YEAR),
}));
Why bad: A reader must mentally execute the filter predicate and the map transform to understa