Provides TypeScript patterns for type-first development, making illegal states unrepresentable, exhaustive handling, and runtime validation. Use when setting up tsconfig.json, creating type definitions, or ensuring type safety in JS/TS codebases.
Type-first JavaScript with strict typing, explicit boundaries, and maintainable module design.
When working with React components (.tsx, .jsx, or files importing React APIs), load react-best-practices alongside this skill. This skill covers TypeScript fundamentals; React-specific guidance belongs in the dedicated React skill.
# Initialize project
bun init --typescript
# Type check
bunx tsc --noEmit
# Run tests
vitest run
Quick Start, Version Coverage, Project Quick PicksType-First Workflow, Make Illegal States Unrepresentable, Exhaustive Control FlowRuntime Validation with Zod, ConfigurationTypeScript 5.x Guidance, TypeScript 6.x Guidance, tsconfig BaselineREFERENCE.mdThis skill now covers both major branches you are likely to encounter:
The core design guidance in this skill applies to both. The main differences are compiler defaults, module-resolution expectations, and deprecated configuration paths.
| Scenario | Start here |
|---|---|
| New Bun app or bundled frontend | Use moduleResolution: "bundler", then follow the TS6 guidance and baseline config. |
| Direct Node.js app or published package | Use moduleResolution: "nodenext", add explicit types, and keep runtime resolution aligned with actual Node behavior. |
| Existing TS5 codebase | Keep strict explicit, adopt satisfies and const type parameters where they improve inference, and migrate deprecated config deliberately. |
Start with the contract, then implement to satisfy it:
Prefer types that prevent invalid combinations at compile time.
// Good: only valid combinations are representable
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
// Bad: allows invalid combinations
// { loading: true, error: Error } is possible
type LooseRequestState<T> = {
loading: boolean;
data?: T;
error?: Error;
};
type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };
const ROLES = ["admin", "user", "guest"] as const;
type Role = typeof ROLES[number];
type CreateUser = {
email: string;
name: string;
};
type UpdateUser = Partial<CreateUser>;
type User = CreateUser & {
id: UserId;
createdAt: Date;
};
strict mode and keep types close to the code they protect.unknown over any at boundaries.satisfies when checking object shapes without widening useful literal inference.const, readonly, and immutable updates by default.TypeScript 5.x projects usually need more explicit compiler guidance.
strict: true explicit in tsconfig.json.moduleResolution: "bundler" for Bun or bundled web apps, and moduleResolution: "nodenext" for direct Node.js packages and apps.satisfies for config objects and lookup tables so shape checking does not widen literals unnecessarily.const type parameters when authoring helpers that should preserve narrow literal inference.verbatimModuleSyntax: true and import type for type-only imports when you care about predictable emitted JavaScript.const routes = {
home: "/",
settings: "/settings",
} satisfies Record<string, `/${string}`>;
function tupleOf<const T extends readonly string[]>(value: T): T {
return value;
}
TypeScript 6.0 keeps the same core type-system practices, but the official defaults and deprecations changed enough that they deserve separate guidance.
strict to true, module to esnext, and types to [].types array intentionally in Node, Bun, and test projects instead of relying on ambient discovery.rootDir explicitly if you expect output rooted at src/.noUncheckedSideEffectImports to be on by default and fix missing or mistyped side-effect imports instead of relying on silent resolution.target: "es5"; TS6 deprecates it.moduleResolution: "node" / "node10"; use bundler or nodenext instead.baseUrl as a default path-alias baseline; prefer runtime-aligned module resolution and only add path mapping deliberately.{
"compilerOptions": {
"rootDir": "./src",
"types": ["node"]
}
}
Every code path should return or throw. Use exhaustive switch statements so new cases fail at compile time instead of runtime.
type Status = "active" | "inactive";
export function processStatus(status: Status): string {
switch (status) {
case "active":
return "processing";
case "inactive":
return "skipped";
default: {
const exhaustiveCheck: never = status;
throw new Error(`Unhandled status: ${exhaustiveCheck}`);
}
}
}
Use schemas as the source of truth for untrusted input and inferred types for the rest of the code.
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1),
createdAt: z.string().transform((value) => new Date(value)),
});
type User = z.infer<typeof UserSchema>;
safeParse when validation failure is expected and needs UI handling.parse at trust boundaries where invalid data means a broken contract..extend(), .pick(), .omit(), .merge(), and .partial() instead of duplicating them.export async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`fetch user ${id} failed: ${response.status}`);
}
const payload: unknown = await response.json();
return UserSchema.parse(payload);
}
Load configuration once at startup, validate it, and pass around a typed config object instead of reading from process.env throughout the codebase.
import { z } from "zod";
const ConfigSchema = z.object({
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
});
export const config = ConfigSchema.parse(process.env);
foo.ts and foo.test.ts).REFERENCE.md instead of bloating the skill entrypoint.{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"strict": true,
"rootDir": "./src",
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"verbatimModuleSyntax": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src"],
"exclude": ["node_modules"]
}
If you are on TS6 and need ambient globals, add a types array explicitly, for example "types": ["node"], "types": ["bun"], or test-runner types as needed.