Technology · TypeScript

TypeScript Performance Tips

Profile code, optimize types, and identify performance bottlenecks in typed applications.

TL;DR
  1. 01Profile TypeScript compilation with diagnostics flags.
  2. 02Avoid deeply nested generic types and large union types.
  3. 03Use type-only imports to reduce bundle size.

Profiling Compilation

  • Enable diagnostics to identify slow parts of compilation.
    tsc --diagnostics
  • Generate a trace file for detailed performance analysis.
    tsc --generateTrace ./trace
    # Open trace in a performance viewer
  • Use incremental compilation for faster rebuilds.
    {
      "compilerOptions": {
        "incremental": true,
        "tsBuildInfoFile": "./build/.tsbuildinfo"
      }
    }
  • Check compilation time with simple commands.
    time tsc

Optimizing Type Definitions

  • Avoid deeply nested generics that slow down type checking.
    // Bad: deeply nested
    type Complex = Array<Record<string, Array<Promise<Result>>>>;
    
    // Good: extract intermediate types
    type ResultPromise = Promise<Result>;
    type ResultMap = Record<string, ResultPromise[]>;
    type Complex = ResultMap[];
  • Keep union types reasonably sized for faster checking.
    // Bad: very large union
    type Status = "a" | "b" | "c"; // ... 100 more members
    
    // Good: group related types
    type SuccessStatus = "ok" | "done";
    type ErrorStatus = "error" | "failed";
    type Status = SuccessStatus | ErrorStatus;
  • Use const assertions to narrow types precisely.
    const roles = ["admin", "user", "guest"] as const;
    type Role = typeof roles[number];
  • Prefer interface over type alias for object shapes — interfaces cache better.
    // interface: TypeScript caches the shape for faster re-use
    interface Config { host: string; port: number; }
    
    // type alias: re-evaluated each time it appears inline
    type Config = { host: string; port: number };
  • Extract shared subtypes to reduce repetition in complex generics.
    // Inline: checked fully every time
    type Response<T> = { data: T; meta: { total: number; page: number } };
    
    // Better: shared subtype is computed once
    type PageMeta = { total: number; page: number };
    type Response<T> = { data: T; meta: PageMeta };

Type-Only Imports

  • Import only types to reduce runtime bundle size.
    import type { User, Product } from "./types";
    
    const user: User = { id: 1, name: "Alice" };
  • Use inline type imports for mixed imports.
    import { readFile, type FileHandle } from "fs/promises";
  • Check for unused imports with ESLint rules.
    {
      "rules": {
        "@typescript-eslint/no-unused-vars": "error"
      }
    }
  • Enable verbatimModuleSyntax to enforce type-only imports at compile time.
    {
      "compilerOptions": {
        "verbatimModuleSyntax": true
      }
    }
  • Use type-only re-exports to avoid importing runtime code unintentionally.
    // Safe: no runtime side effect
    export type { User } from "./models/user";
    
    // May import the whole module at runtime
    export { User } from "./models/user";

Structural Sharing

  • Use interfaces instead of types for declaration merging.
    interface User {
      id: number;
    }
    
    interface User {
      name: string;
    }
    // User now has both id and name
  • Share type definitions across modules by extending base interfaces.
    // Reuse base types across the codebase
    interface BaseEntity {
      id: number;
      createdAt: Date;
    }
    
    interface User extends BaseEntity {
      name: string;
    }
  • Avoid duplicating type definitions — create a shared types file instead.
    // types/shared.ts
    export interface Pagination { page: number; size: number; total: number; }
    
    // Use in multiple files without re-declaring
    import type { Pagination } from "../types/shared";
  • Use intersection types to compose existing types without duplication.
    type Audited = { createdBy: string; updatedBy: string };
    type Product = { id: number; name: string };
    type AuditedProduct = Product & Audited;
  • Use mapped types to derive types from a single source of truth.
    interface User { id: number; name: string; email: string; }
    
    type UserPartial = Partial<User>;    // all fields optional
    type UserReadonly = Readonly<User>;  // all fields readonly

Build Tool Integration

  • Use esbuild for faster bundling with TypeScript.
    npm install -D esbuild
    esbuild src/index.ts --bundle --outfile=dist/index.js
  • Enable skipLibCheck to skip type checking of dependencies.
    {
      "compilerOptions": {
        "skipLibCheck": true
      }
    }
  • Use ts-loader cache in webpack for faster rebuilds.
    {
      loader: 'ts-loader',
      options: {
        experimentalWatchApi: true,
        transpileOnly: true
      }
    }
  • Use SWC or Babel for transpile-only builds and run tsc separately for types.
    # Transpile fast with SWC, check types separately
    swc src -d dist
    tsc --noEmit  # only type checking, no output
  • Use project references to split large monorepos into independently compiled units.
    {
      "references": [
        { "path": "./packages/core" },
        { "path": "./packages/ui" }
      ],
      "compilerOptions": { "composite": true }
    }

Tip: Profile your TypeScript compilation regularly to catch performance regressions early — small type optimizations add up.

Warning: Don't sacrifice type safety for build speed — focus on structural improvements, not disabling type checking.

TypeScript Null SafetyTypeScript Strict Mode