TypeScript
Purpose
Use the type system to make invalid states unrepresentable. This skill covers strict-mode TypeScript, the advanced type features worth reaching for, and the discipline of validating untrusted data exactly once — at the boundary.
When to Use
- Starting a TypeScript project or tightening
tsconfig.json.
- Removing
any and unchecked casts from an existing codebase.
- Modeling a domain with discriminated unions and exhaustive matching.
- Writing a typed API client or SDK.
- Debugging inference failures in generic code.
Capabilities
- Strict compiler configuration and incremental adoption paths.
- Discriminated unions, template literal types, conditional and mapped types.
- Generic constraints, inference control, and
satisfies.
- Runtime schema validation with Zod, wired to inferred static types.
- Type-safe error handling with
Result-style unions.
Inputs
- Source files and the current
tsconfig.json.
- Runtime target (Node, browser, edge, Deno, Bun).
- External data shapes: API responses, environment variables, user input.
Outputs
- Source that compiles under
strict: true with zero any.
- Schemas for every trust boundary, with static types derived from them.
- A
tsconfig.json reflecting the target runtime.
Workflow
- Set the gate — Enable
strict, noUncheckedIndexedAccess, and exactOptionalPropertyTypes. Everything else follows from the compiler's complaints.
- Type the boundaries — Define schemas for every input that crosses into your program: HTTP payloads, env vars, config files, message queues.
- Model the domain — Replace boolean flags and optional grab-bags with discriminated unions.
- Implement inward — Internal code trusts its types because the boundary already validated them.
- Verify —
tsc --noEmit and lint with @typescript-eslint, no-explicit-any set to error.
Best Practices
- Never use
as to silence the compiler. It is an assertion, not a check; it lies at runtime.
- Prefer
unknown over any at every boundary, then narrow.
- Derive types from schemas (
z.infer), never maintain both by hand.
- Use
satisfies to validate an object literal against a type while preserving its narrow inferred type.
- Add an exhaustiveness check (
never) to every union switch — it turns a future missing case into a compile error.
- Do not export types you do not intend to support. A public type is an API contract.
Examples
Boundary validation with derived types:
import { z } from "zod";
const Config = z.object({
port: z.coerce.number().int().positive(),
databaseUrl: z.string().url(),
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
});
export type Config = z.infer<typeof Config>;
export function loadConfig(env: NodeJS.ProcessEnv): Config {
const parsed = Config.safeParse({
port: env.PORT,
databaseUrl: env.DATABASE_URL,
logLevel: env.LOG_LEVEL,
});
if (!parsed.success) {
throw new Error(`Invalid configuration:\n${parsed.error.message}`);
}
return parsed.data;
}
Exhaustive discriminated union:
type Job =
| { status: "queued"; queuedAt: Date }
| { status: "running"; startedAt: Date; workerId: string }
| { status: "failed"; error: string; attempts: number };
function describe(job: Job): string {
switch (job.status) {
case "queued":
return `Queued at ${job.queuedAt.toISOString()}`;
case "running":
return `Running on ${job.workerId}`;
case "failed":
return `Failed after ${job.attempts} attempts: ${job.error}`;
default: {
const unreachable: never = job;
throw new Error(`Unhandled job status: ${JSON.stringify(unreachable)}`);
}
}
}
Notes
noUncheckedIndexedAccess is the single highest-value flag most codebases are missing: it makes arr[i] return T | undefined, which is the truth.
- Declaration files (
.d.ts) from DefinitelyTyped are frequently wrong. Verify against runtime behavior before trusting them.
- Type-level programming is a cost. If a conditional type takes more than a minute to read, prefer a simpler runtime check.
1---2name: typescript3description: Use when writing or hardening TypeScript in strict mode. Covers advanced types, discriminated unions, runtime validation at trust boundaries, generics, and removing `any` from an existing codebase.4---56# TypeScript78## Purpose910Use the type system to make invalid states unrepresentable. This skill covers strict-mode TypeScript, the advanced type features worth reaching for, and the discipline of validating untrusted data exactly once — at the boundary.1112## When to Use1314- Starting a TypeScript project or tightening `tsconfig.json`.15- Removing `any` and unchecked casts from an existing codebase.16- Modeling a domain with discriminated unions and exhaustive matching.17- Writing a typed API client or SDK.18- Debugging inference failures in generic code.1920## Capabilities2122- Strict compiler configuration and incremental adoption paths.23- Discriminated unions, template literal types, conditional and mapped types.24- Generic constraints, inference control, and `satisfies`.25- Runtime schema validation with Zod, wired to inferred static types.26- Type-safe error handling with `Result`-style unions.2728## Inputs2930- Source files and the current `tsconfig.json`.31- Runtime target (Node, browser, edge, Deno, Bun).32- External data shapes: API responses, environment variables, user input.3334## Outputs3536- Source that compiles under `strict: true` with zero `any`.37- Schemas for every trust boundary, with static types derived from them.38- A `tsconfig.json` reflecting the target runtime.3940## Workflow41421. **Set the gate** — Enable `strict`, `noUncheckedIndexedAccess`, and `exactOptionalPropertyTypes`. Everything else follows from the compiler's complaints.432. **Type the boundaries** — Define schemas for every input that crosses into your program: HTTP payloads, env vars, config files, message queues.443. **Model the domain** — Replace boolean flags and optional grab-bags with discriminated unions.454. **Implement inward** — Internal code trusts its types because the boundary already validated them.465. **Verify** — `tsc --noEmit` and lint with `@typescript-eslint`, `no-explicit-any` set to error.4748## Best Practices4950- Never use `as` to silence the compiler. It is an assertion, not a check; it lies at runtime.51- Prefer `unknown` over `any` at every boundary, then narrow.52- Derive types from schemas (`z.infer`), never maintain both by hand.53- Use `satisfies` to validate an object literal against a type while preserving its narrow inferred type.54- Add an exhaustiveness check (`never`) to every union switch — it turns a future missing case into a compile error.55- Do not export types you do not intend to support. A public type is an API contract.5657## Examples5859**Boundary validation with derived types:**6061```ts62import { z } from "zod";6364const Config = z.object({65 port: z.coerce.number().int().positive(),66 databaseUrl: z.string().url(),67 logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),68});6970export type Config = z.infer<typeof Config>;7172export function loadConfig(env: NodeJS.ProcessEnv): Config {73 const parsed = Config.safeParse({74 port: env.PORT,75 databaseUrl: env.DATABASE_URL,76 logLevel: env.LOG_LEVEL,77 });78 if (!parsed.success) {79 throw new Error(`Invalid configuration:\n${parsed.error.message}`);80 }81 return parsed.data;82}83```8485**Exhaustive discriminated union:**8687```ts88type Job =89 | { status: "queued"; queuedAt: Date }90 | { status: "running"; startedAt: Date; workerId: string }91 | { status: "failed"; error: string; attempts: number };9293function describe(job: Job): string {94 switch (job.status) {95 case "queued":96 return `Queued at ${job.queuedAt.toISOString()}`;97 case "running":98 return `Running on ${job.workerId}`;99 case "failed":100 return `Failed after ${job.attempts} attempts: ${job.error}`;101 default: {102 const unreachable: never = job;103 throw new Error(`Unhandled job status: ${JSON.stringify(unreachable)}`);104 }105 }106}107```108109## Notes110111- `noUncheckedIndexedAccess` is the single highest-value flag most codebases are missing: it makes `arr[i]` return `T | undefined`, which is the truth.112- Declaration files (`.d.ts`) from `DefinitelyTyped` are frequently wrong. Verify against runtime behavior before trusting them.113- Type-level programming is a cost. If a conditional type takes more than a minute to read, prefer a simpler runtime check.