| name | typescript-best-practices |
| category | Foundations |
| description | MUST USE when writing or editing TypeScript types, interfaces, generics, type guards, error handling patterns, or tsconfig configuration. Enforces TypeScript 7.x strict-mode best practices and type safety patterns. |
TypeScript Best Practices (TypeScript 7.x)
Type Design
Interface for objects, Type for everything else
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }
type Result = Success | Failure;
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
Discriminated Unions over Optional Properties
interface Shape { kind: string; radius?: number; width?: number; height?: number; }
interface Circle { kind: "circle"; radius: number; }
interface Rectangle { kind: "rectangle"; width: number; height: number; }
type Shape = Circle | Rectangle;
Branded Types
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type ProductId = Brand<string, "ProductId">;
function getUser(id: UserId) {}
getUser(createProductId("prod_123"));
as const
const ROLES = ["admin", "user", "guest"] as const;
type Role = (typeof ROLES)[number];
Utility Types
type UserPreview = Pick<User, "id" | "name">;
type CreateUserInput = Omit<User, "id" | "createdAt">;
type UpdateUserInput = Partial<Omit<User, "id">>;
type RolePermissions = Record<User["role"], string[]>;
Type Safety
Avoid any — use unknown
function parseJSON(json: string): any { return JSON.parse(json); }
function parseJSON(json: string): unknown { return JSON.parse(json); }
const data = parseJSON('{}');
if (typeof data === "object" && data !== null && "name" in data) {
console.log(data.name);
}
Type Guards
const user = value as User;
function isUser(value: unknown): value is User {
return typeof value === "object" && value !== null && "id" in value && "name" in value;
}
if (isUser(value)) console.log(value.name);
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) throw new Error("Expected User");
}
Exhaustive Checks with never
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
function getLabel(status: Status): string {
switch (status) {
case "pending": return "Pending";
case "active": return "Active";
case "archived": return "Archived";
default: return assertNever(status);
}
}
satisfies — validate + preserve inference
const colors: Record<string, string | [number, number, number]> = { red: [255, 0, 0], green: "#00ff00" };
colors.red.toUpperCase();
const colors = { red: [255, 0, 0], green: "#00ff00" } satisfies Record<string, string | [number, number, number]>;
colors.green.toUpperCase();
as const satisfies — lock literals and validate
const config = {
retries: 3,
mode: "strict",
} as const satisfies AppConfig;
config.mode;
Validate External Data (Zod)
const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email() });
type User = z.infer<typeof UserSchema>;
async function fetchUser(): Promise<User> {
const data = await fetch("/api/user").then(r => r.json());
return UserSchema.parse(data);
}
Function Patterns
function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]): number | number[] {
return Array.isArray(input) ? input.map(Number) : Number(input);
}
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }
function asTuple<const T extends readonly unknown[]>(t: T): T { return t; }
const pair = asTuple(["a", "b"]);
interface CreateUserParams { name: string; email: string; role: "admin" | "user"; }
function createUser(params: CreateUserParams) {}
export function fetchUsers(): Promise<User[]> { return api.get("/users"); }
Type-Only Imports
With verbatimModuleSyntax (see tsconfig), imports used only as types must be marked type — they are erased from the JS output, preventing accidental runtime imports and breaking import cycles.
import type { User } from "./types";
import { type Role, createUser } from "./users";
Resource Management — using
using (and await using) auto-dispose a resource when the scope exits, even on throw. The resource implements Symbol.dispose / Symbol.asyncDispose.
function openConn(url: string) {
const conn = connect(url);
return { conn, [Symbol.dispose]() { conn.close(); } };
}
function query() {
using db = openConn(DATABASE_URL);
return db.conn.run("SELECT 1");
}
Common Pitfalls
Object.keys returns string[]
function typedKeys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
Array.filter needs type predicates
const filtered = values.filter(v => v != null);
function nonNullable<T>(value: T): value is NonNullable<T> { return value != null; }
const filtered = values.filter(nonNullable);
Prefer unions over enums
enum Direction { Up, Down }
move(99);
type Direction = "up" | "down" | "left" | "right";
const Status = { Active: "active", Inactive: "inactive" } as const;
type Status = (typeof Status)[keyof typeof Status];
Error Handling — Result Pattern
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
function ok<T>(value: T): Result<T, never> { return { ok: true, value }; }
function err<E>(error: E): Result<never, E> { return { ok: false, error }; }
type AppError =
| { type: "NOT_FOUND"; resource: string }
| { type: "VALIDATION"; fields: string[] }
| { type: "UNAUTHORIZED"; reason: string };
async function fetchUser(id: string): Promise<Result<User, AppError>> {
const res = await fetch(`/api/users/${id}`);
if (res.status === 404) return err({ type: "NOT_FOUND", resource: "User" });
if (res.status === 401) return err({ type: "UNAUTHORIZED", reason: "Invalid token" });
return ok(UserSchema.parse(await res.json()));
}
Recommended tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"target": "ES2022",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"paths": { "@/*": ["./src/*"] }
}
}
Under TypeScript 7.0 (current stable, GA July 8 2026), tsc is the native Go compiler — a plain npm i -D typescript installs it and type-checks ~10× faster; the old tsgo / @typescript/native-preview preview package is now just tsc. strict, module: "esnext", and a modern target are defaults — the block above keeps them explicit for clarity and older toolchains. moduleResolution values node / node10 / classic and baseUrl remain deprecated; paths works without baseUrl. Caveat: 7.0 ships no stable programmatic compiler API yet (lands in 7.1, ~Oct 2026), so editor tooling that depends on it — Vue / Svelte / Astro / Angular template type-checking — may need to stay on 6.x until then.
Rules
- Always enable
strict: true and noUncheckedIndexedAccess
- Always use
unknown instead of any — narrow with type guards
- Always use discriminated unions for multi-shape types
- Always use exhaustive
never checks in switch statements
- Always validate external data at boundaries (Zod/valibot)
- Always annotate return types on exported functions
- Never use numeric enums — use union types or const objects
- Never use type assertions (
as) when a type guard is possible
- Prefer
satisfies over type annotations when you need both validation and inference
- Prefer
as const for literal arrays and config objects; as const satisfies T when you also need a shape check
- Prefer
const type parameters over caller-side as const for literal-preserving generics
- Use
using / await using for resources that need deterministic cleanup
- Mark type-only imports with
import type — required under verbatimModuleSyntax