lean-typescript
Skill4 starsChanged 8 months agoMITView on GitHub
TypeScript 2026 best practices for writing small, powerful codebases. Anti-bloat patterns, schema-first validation, modern toolchain (pnpm, tsx, Vitest 4, Biome), and architectural patterns that both humans and AI agents can reason about. Use this skill whenever writing TypeScript, reviewing TypeScript code, setting up a new TS project, choosing libraries, configuring tsconfig, debloating a codebase, or when the user mentions: TypeScript, Zod, Vitest, pnpm, "too much code", "clean up", "reduce bundle", "strict mode", "type safety", "modern stack", "2026 best practices", or any request involving TypeScript architecture, tooling, or code quality.
In this repository4 other files it gives its agents.
- debug-loopSkill
- implementSkill
- roadmapSkill
- spec-drivenSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 2 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 16
Write less code that does more.
- Distinctive: line 45
Default exports — kills tree-shaking, worse DX for refactoring
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Lean TypeScript
Write less code that does more. Every line is a liability — TypeScript's type system is your primary tool for eliminating runtime bloat.
10 Golden Rules
- Files under 150 lines — forces modular thinking; split by domain concern
- Functions over classes — unless state + methods genuinely belong together
- Interfaces over class hierarchies — simpler contracts, easier testing
- Result types over exceptions — explicit error flow, no hidden control jumps
- Zod at boundaries only — trust static types internally, validate external data
- Named exports only — better tree-shaking and refactoring
- Feature folders — group by domain (
payments/,auth/), not layer (services/,models/) - Pure core, I/O shell — business logic as pure functions, I/O at the edges
- pnpm + tsx + Vitest — the 2026 standard toolchain
- Reject LLM bloat — stay vigilant against additive patterns
Anti-Bloat: Patterns to Reject
LLMs default to these — catch and remove them:
utils.tsorhelpers.ts— be specific about what the module does- Try-catch wrapping every async function — handle errors at boundaries
console.logscattered everywhere — use structured logging or nothing- Runtime type checking what TypeScript already guarantees — trust your types
- Classes with only a constructor and one method — use a function
- Default exports — kills tree-shaking, worse DX for refactoring
- JSDoc that restates the type signature — the types are the docs
- Manager → Service → Handler hierarchies — flatten to functions + interfaces
- Config options for things that never change — hardcode them
anytype — always find the real type or useunknown+ narrowing
Core Patterns
Result Type (railway-oriented)
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 });For heavier use cases, neverthrow (~2KB gz) provides ResultAsync with map/andThen chaining. Only reach for Effect-TS if you need its full fiber runtime — it carries ~5-10KB overhead and requires full buy-in.
Dependency Injection via Interfaces
interface PaymentProvider {
charge(amount: number, token: string): Promise<string>;
}
// Swap implementations without changing callers
async function processPayment(
provider: PaymentProvider, amount: number, token: string
): Promise<string> {
return provider.charge(amount, token);
}Zod at Boundaries
import { z } from 'zod';
// Validate external data at the edge
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
}).strict();
type CreateUser = z.infer<typeof CreateUserSchema>;
// Internal functions trust types — no re-validation
function saveUser(data: CreateUser): Promise<User> {
return db.users.insert(data);
}Branded Types for Domain Safety
const UserId = z.string().uuid().brand('UserId');
type UserId = z.infer<typeof UserId>;
// Compiler prevents mixing UserId with OrderId
function getUser(id: UserId): Promise<User> { /* ... */ }Functional Core, Imperative Shell
// CORE — pure, easily tested, no I/O
function calculateBudget(clicks: number, cpc: number, mult: number): number {
return clicks * cpc * mult;
}
// SHELL — handles I/O, minimal logic
async function updateBudgetEndpoint(req: Request): Promise<Response> {
const metrics = await fetchMetrics(req.params.id);
const budget = calculateBudget(metrics.clicks, metrics.cpc, 1.2);
await saveBudget(req.params.id, budget);
return Response.json({ budget });
}Type-Safe Event System
type EventMap = {
'user.created': { id: string; email: string };
'order.completed': { id: string; total: number };
};
type EventHandler<T extends keyof EventMap> = (data: EventMap[T]) => void;Project Structure Template
src/
├── payments/ # Feature domain
│ ├── types.ts # Domain types + Zod schemas
│ ├── stripe.ts # Provider implementation
│ ├── api.ts # HTTP routes
│ └── events.ts # Domain events
├── auth/
│ ├── types.ts
│ ├── oauth.ts
│ └── api.ts
└── shared/ # Only truly shared code
├── result.ts # Result type utilities
└── config.ts # App configurationNot this: models/ + services/ + controllers/ + utils/ (layer-based = coupling magnet)
Toolchain & Config
For detailed toolchain setup, library choices, and debloating strategies, read the
reference files in references/:
references/toolchain.md— pnpm, tsx, Vitest 4, Biome 2.3, strict tsconfig, package.json templatereferences/patterns.md— advanced DI patterns (Awilix, InversifyJS), testing strategies, property-based testing with fast-check, architecture testingreferences/debloating.md— library replacements (es-toolkit, Zod v4 mini, Temporal API), dead code elimination with Knip, bundle analysis
Quick Decision Guide
| Question | Answer |
| Class or function? | Function, unless genuine state + methods |
| Zod or manual validation? | Zod at boundaries, nothing internally |
| Effect-TS or neverthrow? | neverthrow unless you need the full runtime |
| Jest or Vitest? | Vitest 4 (10-20x faster) |
| ESLint+Prettier or Biome? | Biome 2.3 (19-100x faster, single config) |
| npm, yarn, or pnpm? | pnpm (faster, disk-efficient, catalogs) |
| lodash or es-toolkit? | es-toolkit (97% smaller, 2-3x faster) |
| moment/dayjs or Temporal? | Temporal API (native, 0KB) if targeting modern runtime |
| Default or named export? | Named. Always. |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

