writing-typescript
TypeScript Development (5.x)
Core Philosophy
-
Strict Mode Always
- Enable all strict checks in tsconfig
- Treat
anyas a bug—useunknownfor untrusted input - noUncheckedIndexedAccess, exactOptionalPropertyTypes
-
Interface vs Type
- interface for object shapes (extensible, mergeable)
- type for unions, intersections, mapped types
- interface for React props and public APIs
-
Discriminated Unions
- Literal
kind/typetag for variants - Exhaustive switch with never check
- Model states as unions, not boolean flags
- Literal
-
Flat Control Flow
- Guard clauses with early returns
- Type guards and predicate helpers
- Maximum 2 levels of nesting
-
Result Type Pattern
- Result<T, E> for explicit error handling
- Discriminated union for success/failure
- Custom Error subclasses for instanceof
Quick Patterns
Discriminated Unions (Not Boolean Flags)
// GOOD: discriminated union for state
type LoadState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
// BAD: boolean flags
type LoadState = {
isLoading: boolean;
isError: boolean;
data: T | null;
error: string | null;
};
Flat Control Flow (No Nesting)
// GOOD: guard clauses, early returns
function process(user: User | null): Result<Data> {
if (!user) return err("no user");
if (!user.isActive) return err("inactive");
if (user.role !== "admin") return err("not admin");
return ok(doWork(user)); // happy path at end
}
// BAD: nested conditions
function process(user: User | null): Result<Data> {
if (user) {
if (user.isActive) {
if (user.role === "admin") {
return ok(doWork(user));
}
}
}
return err("invalid");
}
Type Guards
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value
);
}
// Predicate helper for flat code
const isActiveAdmin = (u: User | null): u is User & { role: "admin" } =>
!!u && u.isActive && u.role === "admin";
Result Type
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
async function fetchUser(
id: string,
): Promise<Result<User, "not-found" | "network">> {
try {
const res = await fetch(`/users/${id}`);
if (res.status === 404) return err("not-found");
if (!res.ok) return err("network");
return ok(await res.json());
} catch {
return err("network");
}
}
Exhaustive Switch
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
case "rect":
return shape.width * shape.height;
default: {
const _exhaustive: never = shape; // Error if variant missed
return _exhaustive;
}
}
}
tsconfig.json Essentials
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noImplicitOverride": true,
"isolatedModules": true
}
}
References
- PATTERNS.md - Code patterns and style
- REACT.md - React component patterns
- TESTING.md - Testing with vitest
Commands
bun install # Install deps
bun run build # Build
bun test # Test
bun run lint # Lint
bun run format # Format
Verify Generated Code
After generating code, always verify it compiles and passes lint:
bunx tsc --noEmit && bun run lint
More from alexei-led/cc-thingz
improving-tests
Improve test design and coverage, including TDD/red-green-refactor guidance. Use when user says "improve tests", "refactor tests", "test coverage", "combine tests", "table-driven", "parametrize", "test.each", "test-first", "TDD", "red-green-refactor", or wants to remove test waste.
4refactoring-code
Batch refactoring via MorphLLM edit_file. Use for "refactor across files", "batch rename", "update pattern everywhere", large files (500+ lines), 5+ edits in same file, or applying an approved architecture-deepening refactor.
3debating-ideas
Dialectic thinking — spawn thesis and antithesis agents to stress-test ideas, then synthesize and verify against code. Use when user says "debate", "argue both sides", "devil's advocate", "stress test this idea", "pros and cons of approach", or wants rigorous evaluation of a design decision.
3linting-instructions
Lint plugin agent/skill prompts against rules derived from Anthropic model cards (Opus 4.6, Sonnet 4.6). Use when authoring or reviewing skills and agents — "lint instructions", "audit prompts", "model card rules".
3learning-patterns
Extract learnings and generate project-specific customizations (CLAUDE.md, commands, skills, hooks). Use when user says "learn", "extract learnings", "what did we learn", "save learnings", "adapt config", or wants to improve Claude Code based on conversation patterns.
3documenting-code
Update project documentation based on recent changes. Use when user says "update docs", "document", "add documentation", "update readme", "write docs", or wants to improve documentation.
3