| name | typescript-effective |
| description | Use when writing production TypeScript — clean code idioms, effective-TS items, strict tsconfig, migration from JS, build performance, testing, and anti-patterns. Load references/typescript-mastery.md for type-system depth and references/typescript-design-patterns.md for GoF patterns. |
| metadata | {"portable":true,"compatible_with":["claude-code","codex"]} |
Effective TypeScript
Acknowledgement: Shared by Peter Bamuhigire, techguypeter.com, +256 784 464178.
Use When
- Use when writing production TypeScript — clean code idioms, effective-TS items, strict tsconfig, migration from JS, build performance, testing, and anti-patterns. Load
references/typescript-mastery.md for type-system depth and references/typescript-design-patterns.md for GoF patterns.
Evidence Produced
| Category | Artifact | Format | Example |
|---|
| Correctness | Strict tsconfig + Zod boundary register | Markdown doc covering strictness flags applied and Zod schemas at module boundaries | docs/ts/strict-config-register.md |
References
- Use the
references/ directory for deep detail after reading the core workflow below.
Production-grade TypeScript beyond the type system. Every rule here lifts real code quality, not just appeases the compiler.
Prerequisite: Load references/typescript-mastery.md for type-system depth. Use this skill for day-to-day production idioms.
When this skill applies
- Code review on TypeScript PRs.
- Starting a new TypeScript project (tsconfig, linting, test setup).
- Migrating a JavaScript codebase to TypeScript gradually.
- Eliminating "any" drift in an existing TS codebase.
- Tuning TypeScript build for large monorepos.
Non-negotiables
strict: true plus noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride.
- Never use
any. Use unknown at boundaries and narrow.
- Never cast with
as unless there is no better option; never as unknown as T.
- Validate every external input with Zod (API responses, env vars, form data, queue payloads).
- Prefer
union types over enum unless integer values and bidirectional lookup are needed.
- Use discriminated unions with exhaustive
switch and assertNever.
- Never throw strings. Throw
Error subclasses.
- Model absence with
null/undefined intentionally — don't mix.
readonly on inputs by default.
- Error handling — prefer Result/Either for expected failures at boundaries.
tsconfig for production
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"isolatedModules":
strict alone is not enough. noUncheckedIndexedAccess catches real bugs (arr[i] is T | undefined). exactOptionalPropertyTypes distinguishes { x?: number } from { x?: number | undefined }. See references/tsconfig-production.md.
Core effective-TS items
Prefer types that narrow your values:
function sendEmail(to: string, subject: string, body: string) { ... }
type Email = string & { readonly __brand: "Email" };
type Subject = string & { readonly __brand: "Subject" };
function parseEmail(raw: string): Email | null { ... }
Discriminated unions + exhaustive match:
type Event =
| { kind: "login"; userId: string }
| { kind: "logout"; userId: string; at: Date }
| { kind: "purchase"; userId: string; amount: number };
function handle(e: Event): string {
switch (e.kind) {
case "login": return `login ${e.userId}`;
case "logout": return `logout ${e.userId}`;
case "purchase": return `bought ${e.amount}`;
default: return assertNever(e);
}
}
function assertNever(x: never): never { throw new Error(`unhandled: ${JSON.stringify(x)}`); }
Narrow unknown before use — never silently trust it.
See references/effective-ts-items.md for the full catalogue (~40 items).
Clean code in TS
- Small functions — one reason to change. Functions over 40 lines or 3 levels of nesting: split.
- Names carry intent —
isActive(user) not check(user); loadUser(id) not get(id).
- Comments explain WHY — TS types explain WHAT. Don't duplicate.
- SOLID in TS — Single Responsibility, Interface Segregation, Dependency Inversion via constructor injection or factory functions.
- Immutability —
readonly, as const, Readonly<T>, spread instead of mutate.
- Pure functions where possible — easier to test, reason about, memoise.
See references/clean-code-ts.md.
Error handling — Result/Either at boundaries
Throwing is fine inside a module; at boundaries (API handlers, workers, UI event handlers), return typed results:
type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E> = Ok<T> | Err<E>;
const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
const err = <E>(error: E): Err<E> => ({ ok: false, error });
async function loadUser(id: string): Promise<Result<User, "not_found" | "db_down">> {
try {
const user = await db.user.findUnique({ where: { id } });
return user ? ok(user) : err("not_found");
} catch {
return err("db_down");
}
}
Libraries: neverthrow, ts-results, or hand-roll as above. Pick one per project. See references/error-handling-result.md.
Zod at boundaries
Every external input parsed with Zod, every internal type inferred from the schema.
import { z } from "zod";
const UserCreate = z.object({
email: z.string().email(),
age: z.number().int().min(0).max(150),
role: z.enum(["admin", "member", "viewer"]),
}).strict();
type UserCreate = z.infer<typeof UserCreate>;
app.post("/users", (req, res) => {
const parsed = UserCreate.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ errors: parsed.error.flatten() });
});
Sources of truth: Zod schema → inferred type. Never define the type separately. See references/zod-boundaries.md.
Migration JS → TS
Gradual, not big-bang. See references/migration-js-to-ts.md for the playbook. Summary:
allowJs: true, checkJs: true — TS checks JS files.
- Add JSDoc types as light annotation.
- Rename
.js → .ts file by file, fix errors.
- Turn on
noImplicitAny, then strict, then stricter flags one at a time.
- Never convert in a single PR more than one team can review.
Build performance
- Project references for monorepos — incremental builds across packages.
isolatedModules lets bundlers type-check per-file.
transpile-only in dev (ts-node with swc/esbuild, tsup).
- Type-check in CI separately from bundling.
- Skip lib check (
skipLibCheck: true) — trust dep types.
See references/build-performance.md.
Testing
- vitest for Node + browser — fast, Jest-compatible, TypeScript-native.
- Type tests with
expectTypeOf (vitest) or tsd for library APIs.
- Property-based tests with
fast-check for pure logic.
- Fixtures strongly typed via schemas — reuse Zod schemas for test data generators.
See references/testing-vitest.md.
Anti-patterns
any — signals "I give up on types." Fix, don't paper over.
as T (type assertion) — silently lies to the compiler.
as unknown as T — the double lie. Always wrong.
! non-null assertion — use narrowing or a type guard.
Function type — use (...args: never[]) => unknown or specific signatures.
Object / {} type — use Record<string, unknown> or a specific shape.
enum for small string sets — use union of string literals.
- Class with all static methods — use a namespace-less module.
namespace — use ES modules.
- Parameter properties in class constructors where they hurt readability.
- Silent
catch (e) {} — log or re-throw.
- Mutating function arguments.
delete obj.key on typed objects — use spread to build a new object.
See references/anti-patterns.md.
CI gates
tsc --noEmit
eslint . --max-warnings=0
vitest run --coverage --coverage.thresholds.lines=80
Read next
references/typescript-mastery.md — deep type system when you need it.
references/typescript-design-patterns.md — GoF patterns in TS.
typescript-full-stack — shared types FE↔BE, Zod everywhere.
react-development / nextjs-app-router — framework-specific.
References
references/tsconfig-production.md
references/effective-ts-items.md
references/clean-code-ts.md
references/error-handling-result.md
references/zod-boundaries.md
references/migration-js-to-ts.md
references/build-performance.md
references/testing-vitest.md
references/anti-patterns.md
Decision Rules
| Condition | Action |
|---|
| Value crosses a runtime trust boundary | Parse it with a runtime schema |
| Type complexity obscures meaning | Use a named type or simpler API |
| Migration is incremental | Tighten checks by boundary |
Capability Contract
Read and search are required. Editing, dependency updates, builds, and tests require authorisation.
Degraded Mode
Fallback: without execution, provide exact type-check, lint, and test commands. Do not claim runtime safety from static types alone.
Inputs
| Artefact | Required? | Purpose |
|---|
| TypeScript target, compiler configuration, API contracts, and code scope | yes | Preserve sound types and compatibility |
Outputs
- Produce type-safe code or review findings with compile and test evidence.