TypeScript
General Principles
Section titled “General Principles”Write type-safe, maintainable TypeScript code that leverages modern language features and provides excellent developer experience.
TypeScript Configuration
Section titled “TypeScript Configuration”Strict Mode (Required)
Section titled “Strict Mode (Required)”Always enable strict mode in tsconfig.json:
{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "exactOptionalPropertyTypes": true }}Modern Configuration
Section titled “Modern Configuration”Use modern module resolution and target:
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "lib": ["ES2022", "DOM", "DOM.Iterable"], "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "isolatedModules": true }}Type Annotations
Section titled “Type Annotations”Explicit Return Types
Section titled “Explicit Return Types”Always provide explicit return types for functions:
// Goodfunction getUser(id: string): User { return database.findUser(id);}
async function fetchData(): Promise<ApiResponse> { return await api.get('/data');}
// Avoidfunction getUser(id: string) { return database.findUser(id);}Variable Type Inference
Section titled “Variable Type Inference”Let TypeScript infer variable types when obvious:
// Good - inference is clearconst name = "John";const count = 42;const items = ["a", "b", "c"];
// Good - explicit when neededconst user: User = createUser();const config: Config | null = null;
// Avoid - unnecessary annotationconst name: string = "John";const count: number = 42;Function Parameters
Section titled “Function Parameters”Always annotate function parameters:
// Goodfunction greet(name: string, age: number): void { console.log(`${name} is ${age} years old`);}
// Badfunction greet(name, age) { console.log(`${name} is ${age} years old`);}Modern TypeScript Features
Section titled “Modern TypeScript Features”Using Declarations (TypeScript 5.2+)
Section titled “Using Declarations (TypeScript 5.2+)”Use using for automatic resource disposal:
// Good - automatic cleanupfunction processFile(path: string): void { using file = openFile(path); // File automatically closed when scope ends}
// With asyncasync function query(): Promise<void> { await using connection = await connectDatabase(); // Connection automatically closed}Satisfies Operator (TypeScript 4.9+)
Section titled “Satisfies Operator (TypeScript 4.9+)”Use satisfies for type checking without widening:
// Good - maintains literal typesconst config = { host: "localhost", port: 3000, features: ["auth", "api"]} satisfies Config;
config.host; // Type: "localhost" (not string)
// Avoid - loses precisionconst config: Config = { host: "localhost", port: 3000, features: ["auth", "api"]};
config.host; // Type: stringTemplate Literal Types
Section titled “Template Literal Types”Use template literals for type-safe strings:
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";type Endpoint = `/api/${string}`;
type Route = `${HttpMethod} ${Endpoint}`;// Type: "GET /api/${string}" | "POST /api/${string}" | ...
function handleRoute(route: Route): void { // Type-safe route handling}Classes and Objects
Section titled “Classes and Objects”Private Fields
Section titled “Private Fields”Use native private fields (#) for true privacy:
// Good - ES2022 private fieldsclass User { #password: string;
constructor(password: string) { this.#password = password; }
validatePassword(input: string): boolean { return this.#password === input; }}
// Also acceptable - TypeScript privateclass User { constructor(private password: string) {}}Class Properties
Section titled “Class Properties”Use property initialization syntax:
class Component { // Good - direct initialization private count = 0; private items: string[] = [];
// Good - constructor parameter properties constructor( private readonly name: string, private readonly config: Config ) {}}Arrow Functions in Classes
Section titled “Arrow Functions in Classes”Use arrow functions for methods that need binding:
// Good - especially in React/Vueclass Component { count = 0;
// Auto-bound method increment = (): void => { this.count++; };
// Regular method for non-bound cases getCount(): number { return this.count; }}Interfaces vs Types
Section titled “Interfaces vs Types”When to Use Each
Section titled “When to Use Each”Use interfaces for object shapes, types for unions and utilities:
// Good - interface for object shapesinterface User { id: string; name: string; email: string;}
// Good - type for unionstype Status = "pending" | "success" | "error";
// Good - type for complex typestype ApiResponse<T> = | { status: "success"; data: T } | { status: "error"; error: string };Interface Extension
Section titled “Interface Extension”Use extends for interface composition:
interface Entity { id: string; createdAt: Date;}
interface User extends Entity { name: string; email: string;}Type Intersection
Section titled “Type Intersection”Use & for type composition:
type Timestamped = { createdAt: Date; updatedAt: Date;};
type User = { name: string; email: string;} & Timestamped;Const Enums
Section titled “Const Enums”Use const enums for compile-time constants:
// Good - inlined by compilerconst enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT"}
// Good - with modern bundlersconst direction = Direction.Up; // Inlined to "UP"String Literal Unions (Preferred)
Section titled “String Literal Unions (Preferred)”For most cases, prefer string literal unions:
// Preferred - simpler and more flexibletype Direction = "up" | "down" | "left" | "right";
const direction: Direction = "up";When to Use Regular Enums
Section titled “When to Use Regular Enums”Use regular enums when you need reverse mapping:
enum HttpStatus { OK = 200, BadRequest = 400, NotFound = 404}
HttpStatus.OK; // 200HttpStatus[200]; // "OK" (reverse mapping)Utility Types
Section titled “Utility Types”Built-in Utilities
Section titled “Built-in Utilities”Leverage TypeScript’s built-in utility types:
// Partial - all properties optionaltype PartialUser = Partial<User>;
// Required - all properties requiredtype RequiredConfig = Required<Config>;
// Pick - select specific propertiestype UserPreview = Pick<User, "id" | "name">;
// Omit - exclude specific propertiestype UserWithoutPassword = Omit<User, "password">;
// Record - key-value maptype UserMap = Record<string, User>;
// ReturnType - extract return typetype Result = ReturnType<typeof fetchData>;Custom Utility Types
Section titled “Custom Utility Types”Create reusable utility types:
// Nullable typetype Nullable<T> = T | null;
// Optional propertiestype Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Deep readonlytype DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];};Type Guards
Section titled “Type Guards”User-Defined Type Guards
Section titled “User-Defined Type Guards”Use is predicates for type narrowing:
// Goodfunction isUser(value: unknown): value is User { return ( typeof value === "object" && value !== null && "id" in value && "name" in value );}
// Usageif (isUser(data)) { console.log(data.name); // Type: User}Assertion Functions
Section titled “Assertion Functions”Use assertion functions for runtime checks:
function assertIsUser(value: unknown): asserts value is User { if (!isUser(value)) { throw new Error("Not a user"); }}
// UsageassertIsUser(data);console.log(data.name); // Type: User (after assertion)Generics
Section titled “Generics”Generic Constraints
Section titled “Generic Constraints”Use constraints for type safety:
// Good - constrained genericfunction getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key];}
// Good - multiple constraintsfunction merge<T extends object, U extends object>(obj1: T, obj2: U): T & U { return { ...obj1, ...obj2 };}Generic Defaults
Section titled “Generic Defaults”Provide sensible defaults:
// Good - default type parameterinterface ApiResponse<T = unknown> { data: T; status: number;}
// Usageconst response: ApiResponse = { data: "hello", status: 200 };const typedResponse: ApiResponse<User> = { data: user, status: 200 };Async/Await
Section titled “Async/Await”Always Type Promises
Section titled “Always Type Promises”Explicitly type Promise return values:
// Goodasync function fetchUser(id: string): Promise<User> { const response = await fetch(`/api/users/${id}`); return response.json();}
// Good - with error handlingasync function fetchUser(id: string): Promise<User | null> { try { const response = await fetch(`/api/users/${id}`); return await response.json(); } catch { return null; }}Promise Utilities
Section titled “Promise Utilities”Use Promise combinators with types:
// Allconst results = await Promise.all<[User, Post[], Comment[]]>([ fetchUser(id), fetchPosts(id), fetchComments(id)]);
// AllSettledconst settled = await Promise.allSettled([ fetchUser("1"), fetchUser("2")]);Nullability
Section titled “Nullability”Avoid Any
Section titled “Avoid Any”Never use any except for migration or FFI:
// Badfunction process(data: any): any { return data.value;}
// Good - use unknownfunction process(data: unknown): string { if (isValidData(data)) { return data.value; } throw new Error("Invalid data");}Null vs Undefined
Section titled “Null vs Undefined”Be consistent with nullability:
// Good - use undefined for optional valuesinterface User { id: string; name: string; email?: string; // Optional, may be undefined}
// Good - use null for explicit absenceinterface ApiResponse { data: User | null; // Explicitly no data error: string | null;}Optional Chaining and Nullish Coalescing
Section titled “Optional Chaining and Nullish Coalescing”Use modern operators for null handling:
// Good - optional chainingconst email = user?.profile?.email;
// Good - nullish coalescingconst name = user?.name ?? "Anonymous";
// Combinedconst display = user?.profile?.displayName ?? user?.name ?? "Guest";Modules and Imports
Section titled “Modules and Imports”Named Exports (Preferred)
Section titled “Named Exports (Preferred)”Prefer named exports for better refactoring:
// Good - named exportsexport function calculateTotal(items: Item[]): number { return items.reduce((sum, item) => sum + item.price, 0);}
export class ShoppingCart { // ...}
// Importimport { calculateTotal, ShoppingCart } from "./cart";Default Exports
Section titled “Default Exports”Use default exports for framework components or single-purpose modules:
// Good - component default exportexport default function UserProfile({ user }: Props): JSX.Element { return <div>{user.name}</div>;}
// Good - single purpose moduleexport default class ApiClient { // ...}Type-Only Imports
Section titled “Type-Only Imports”Use type modifier for type imports:
// Good - explicit type importsimport type { User, Post } from "./types";import { fetchUser } from "./api";
// Good - mixed importsimport { fetchUser, type ApiResponse } from "./api";Error Handling
Section titled “Error Handling”Type-Safe Error Handling
Section titled “Type-Safe Error Handling”Use discriminated unions for error handling:
// Good - Result type patterntype Result<T, E = Error> = | { success: true; data: T } | { success: false; error: E };
async function fetchUser(id: string): Promise<Result<User>> { try { const response = await fetch(`/api/users/${id}`); const data = await response.json(); return { success: true, data }; } catch (error) { return { success: false, error: error as Error }; }}
// Usageconst result = await fetchUser("123");if (result.success) { console.log(result.data.name);} else { console.error(result.error.message);}Custom Error Classes
Section titled “Custom Error Classes”Create typed error classes:
class ValidationError extends Error { constructor( message: string, public field: string ) { super(message); this.name = "ValidationError"; }}
class ApiError extends Error { constructor( message: string, public statusCode: number ) { super(message); this.name = "ApiError"; }}Naming Conventions
Section titled “Naming Conventions”General Rules
Section titled “General Rules”Use clear, descriptive names:
// Goodinterface UserProfile { displayName: string; avatarUrl: string;}
function calculateTotalPrice(items: CartItem[]): number { // ...}
// Badinterface UP { dn: string; au: string;}
function calc(i: CartItem[]): number { // ...}Naming Patterns
Section titled “Naming Patterns”- PascalCase - Classes, interfaces, types, enums, type parameters
- camelCase - Variables, functions, methods, parameters
- UPPER_SNAKE_CASE - Constants
- lowercase - File names with hyphens
// Classes and typesclass UserService {}interface ApiResponse {}type HttpMethod = "GET" | "POST";
// Variables and functionsconst userName = "John";function fetchData(): void {}
// Constantsconst MAX_RETRY_ATTEMPTS = 3;const API_BASE_URL = "https://api.example.com";
// File names// user-service.ts// api-client.ts// http-utils.tsBest Practices
Section titled “Best Practices”✅ Enable strict mode in tsconfig.json
✅ Use explicit return types for functions
✅ Leverage modern TypeScript features (using, satisfies)
✅ Use native private fields (#) for true encapsulation
✅ Prefer unknown over any
✅ Use const enums with modern bundlers
✅ Create type guards for runtime validation
✅ Use utility types to avoid repetition
✅ Provide type constraints for generics
✅ Use discriminated unions for state management
Don’ts
Section titled “Don’ts”❌ Use any (use unknown instead)
❌ Skip return type annotations
❌ Use as casts without validation
❌ Ignore TypeScript errors
❌ Use non-null assertion (!) carelessly
❌ Disable strict mode
❌ Mix module systems (ESM/CommonJS)
❌ Use type assertions for coercion
❌ Create overly complex type hierarchies
Tools and Ecosystem
Section titled “Tools and Ecosystem”Linting and Formatting
Section titled “Linting and Formatting”- ESLint - Code linting with typescript-eslint
- Prettier - Code formatting
- ts-prune - Find unused exports
- tsc-watch - Watch mode compilation
Type Checking
Section titled “Type Checking”- tsc - TypeScript compiler
- ts-node - Execute TypeScript directly
- tsx - Fast TypeScript execution
Configuration Example
Section titled “Configuration Example”.eslintrc.json:
{ "parser": "@typescript-eslint/parser", "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking" ], "parserOptions": { "project": "./tsconfig.json" }, "rules": { "@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/strict-boolean-expressions": "error" }}