TypeScript Best Practices
Type Safety
- Enable
strict: true in tsconfig.json — never disable strict checks in production code.
- Prefer
unknown over any. If any is unavoidable, add a comment explaining why and narrow it immediately.
- Use
satisfies to validate a value matches a type while preserving its narrowed literal type:
const config = {
endpoint: "/api/users",
timeout: 3000,
} satisfies Config;
- Prefer type narrowing (type guards,
in operator, instanceof) over type assertions (as).
Type Inference
- Let TypeScript infer when the type is obvious — don't annotate what the compiler already knows:
// Redundant
const name: string = "Graham";
// Let it infer
const name = "Graham";
- Always annotate function return types for exported/public functions — it catches accidental return type changes and improves IDE performance:
export function getUser(id: string): User | undefined {
return users.get(id);
}
- Annotate function parameters — they cannot be inferred from implementation.
Types vs Interfaces
- Use
type for unions, intersections, mapped types, and utility types.
- Use
interface for object shapes that may be extended or implemented.
- Be consistent within a codebase — pick one default and stick with it.
Enums and Constants
- Prefer
as const objects over enum:
const Status = {
Active: "active",
Inactive: "inactive",
} as const;
type Status = (typeof Status)[keyof typeof Status];
- This gives you type safety, tree-shaking, and no runtime enum overhead.
Null Handling
- Prefer explicit
| undefined in types over optional properties when the distinction matters.
- Use optional chaining (
?.) and nullish coalescing (??) over manual null checks.
- Avoid non-null assertions (
!) — narrow the type instead.
Generics
- Name generic parameters descriptively when there are multiple:
TInput, TOutput instead of T, U.
- Constrain generics with
extends to communicate intent:
function merge<T extends Record<string, unknown>>(a: T, b: Partial<T>): T {
return { ...a, ...b };
}
- Avoid over-genericizing — if a function only ever handles one type, don't make it generic.
Utility Types
- Use built-in utility types (
Partial, Required, Pick, Omit, Record, Readonly) instead of reimplementing them.
Readonly<T> for data that should not be mutated.
Pick and Omit to derive subsets from existing types rather than duplicating fields.
Error Handling
- Type errors explicitly — don't rely on
catch (e) being any:
try {
await fetchData();
} catch (error) {
if (error instanceof ApiError) {
handleApiError(error);
}
throw error;
}
- Create typed error classes for domain-specific errors.
Module Organization
- One type/interface per concern — avoid monolithic
types.ts files.
- Co-locate types with the code that uses them.
- Export types from barrel files only when they form part of the public API.
- Use
import type / export type for type-only imports to enable proper tree-shaking.
Naming Conventions
- PascalCase for types, interfaces, enums, and classes.
- camelCase for variables, functions, and methods.
- UPPER_SNAKE_CASE for true constants (compile-time values).
- Don't prefix interfaces with
I or types with T — it's not C#.
1---2name: typescript-best-practices3description: Core TypeScript conventions for type safety, inference, and clean code. Use when writing TypeScript, reviewing TypeScript code, creating interfaces/types, or when the user asks about TypeScript patterns, conventions, or best practices.4---56# TypeScript Best Practices78## Type Safety910- Enable `strict: true` in `tsconfig.json` — never disable strict checks in production code.11- Prefer `unknown` over `any`. If `any` is unavoidable, add a comment explaining why and narrow it immediately.12- Use `satisfies` to validate a value matches a type while preserving its narrowed literal type:1314```typescript15const config = {16 endpoint: "/api/users",17 timeout: 3000,18} satisfies Config;19```2021- Prefer type narrowing (type guards, `in` operator, `instanceof`) over type assertions (`as`).2223## Type Inference2425- Let TypeScript infer when the type is obvious — don't annotate what the compiler already knows:2627```typescript28// Redundant29const name: string = "Graham";3031// Let it infer32const name = "Graham";33```3435- Always annotate function return types for exported/public functions — it catches accidental return type changes and improves IDE performance:3637```typescript38export function getUser(id: string): User | undefined {39 return users.get(id);40}41```4243- Annotate function parameters — they cannot be inferred from implementation.4445## Types vs Interfaces4647- Use `type` for unions, intersections, mapped types, and utility types.48- Use `interface` for object shapes that may be extended or implemented.49- Be consistent within a codebase — pick one default and stick with it.5051## Enums and Constants5253- Prefer `as const` objects over `enum`:5455```typescript56const Status = {57 Active: "active",58 Inactive: "inactive",59} as const;6061type Status = (typeof Status)[keyof typeof Status];62```6364- This gives you type safety, tree-shaking, and no runtime enum overhead.6566## Null Handling6768- Prefer explicit `| undefined` in types over optional properties when the distinction matters.69- Use optional chaining (`?.`) and nullish coalescing (`??`) over manual null checks.70- Avoid non-null assertions (`!`) — narrow the type instead.7172## Generics7374- Name generic parameters descriptively when there are multiple: `TInput`, `TOutput` instead of `T`, `U`.75- Constrain generics with `extends` to communicate intent:7677```typescript78function merge<T extends Record<string, unknown>>(a: T, b: Partial<T>): T {79 return { ...a, ...b };80}81```8283- Avoid over-genericizing — if a function only ever handles one type, don't make it generic.8485## Utility Types8687- Use built-in utility types (`Partial`, `Required`, `Pick`, `Omit`, `Record`, `Readonly`) instead of reimplementing them.88- `Readonly<T>` for data that should not be mutated.89- `Pick` and `Omit` to derive subsets from existing types rather than duplicating fields.9091## Error Handling9293- Type errors explicitly — don't rely on `catch (e)` being `any`:9495```typescript96try {97 await fetchData();98} catch (error) {99 if (error instanceof ApiError) {100 handleApiError(error);101 }102 throw error;103}104```105106- Create typed error classes for domain-specific errors.107108## Module Organization109110- One type/interface per concern — avoid monolithic `types.ts` files.111- Co-locate types with the code that uses them.112- Export types from barrel files only when they form part of the public API.113- Use `import type` / `export type` for type-only imports to enable proper tree-shaking.114115## Naming Conventions116117- PascalCase for types, interfaces, enums, and classes.118- camelCase for variables, functions, and methods.119- UPPER_SNAKE_CASE for true constants (compile-time values).120- Don't prefix interfaces with `I` or types with `T` — it's not C#.