TypeScript Performance Tips

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

TL;DR

  1. 01Profile build time using tsc --diagnostics and --generateTrace flags.
  2. 02Avoid deeply nested generic types and large union types.
  3. 03Use import type to eliminate runtime overhead in compiled output.

Tips

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

Warnings

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

Profiling Compilation

  • Run tsc with diagnostics to measure time spent in each compilation phase.

    tsc --diagnostics
    # Prints: Files, Lines, Nodes, Identifiers, Symbols, Types, Check time
    
  • Generate a trace file and open it in Chrome for deep performance analysis.

    tsc --generateTrace ./trace
    # Open chrome://tracing and load ./trace/trace.json
    
  • Enable incremental compilation to cache results and skip unchanged files.

    {
      "compilerOptions": {
        "incremental": true,
        "tsBuildInfoFile": "./build/.tsbuildinfo"
      }
    }
    
  • Measure total build time with the time command to establish a baseline.

    time tsc
    # real 0m4.321s — record this before making changes
    
  • Use extendedDiagnostics for a detailed per-phase breakdown of build time.

    tsc --extendedDiagnostics
    # Outputs: I/O Read, I/O Write, Parse, Bind, Check, Emit
    

Optimizing Type Definitions

  • Avoid deeply nested generics — extract named intermediate types to reduce compiler work.

    // Bad: deeply nested forces repeated re-evaluation
    type Complex = Array<Record<string, Array<Promise<Result>>>>;
    
    // Good: named intermediate types are cached by the compiler
    type ResultPromise = Promise<Result>;
    type ResultMap = Record<string, ResultPromise[]>;
    type Complex = ResultMap[];
    
  • Keep union types small by grouping related members into subtypes.

    // Bad: large union is slow to distribute over
    type Status = "ok" | "done" | "error" | "failed"; // ... many more
    
    // Good: grouped subtypes are faster to check
    type SuccessStatus = "ok" | "done";
    type ErrorStatus = "error" | "failed";
    type Status = SuccessStatus | ErrorStatus;
    
  • Use const assertions to narrow literal types without adding runtime code.

    const roles = ["admin", "user", "guest"] as const;
    type Role = typeof roles[number]; // "admin" | "user" | "guest"
    
  • Prefer interface over type alias for object shapes — interfaces are cached by TypeScript.

    // 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 avoid repeating complex inline shapes in generics.

    // Better: shared subtype is computed and cached once
    type PageMeta = { total: number; page: number };
    type Response<T> = { data: T; meta: PageMeta };
    

Type-Only Imports

  • Use import type to import types only — the compiler erases them at emit time.

    import type { User, Product } from "./types";
    
    const user: User = { id: 1, name: "Alice" };
    
  • Use inline type imports when mixing value and type imports from one module.

    import { readFile, type FileHandle } from "fs/promises";
    // readFile is a runtime value; FileHandle is erased at emit
    
  • Enable verbatimModuleSyntax in tsconfig to enforce type-only imports at compile time.

    {
      "compilerOptions": {
        "verbatimModuleSyntax": true
      }
    }
    
  • Use type-only re-exports to avoid pulling in runtime module side effects.

    // Safe: no runtime import of the whole module
    export type { User } from "./models/user";
    
    // Risky: may import the entire module at runtime
    export { User } from "./models/user";
    
  • Enforce no unused imports with ESLint to keep compiled output lean.

    {
      "rules": {
        "@typescript-eslint/no-unused-vars": "error"
      }
    }
    

Structural Sharing

  • Use declaration merging to extend an interface across multiple files cleanly.

    interface User { id: number; }
    
    interface User { name: string; }
    // User now has both id and name — no duplication needed
    
  • Extend base interfaces to share common fields across all entity types.

    interface BaseEntity { id: number; createdAt: Date; }
    
    interface User extends BaseEntity { name: string; }
    interface Post extends BaseEntity { title: string; }
    
  • Create a shared types file to avoid re-declaring the same shapes.

    // types/shared.ts
    export interface Pagination { page: number; size: number; total: number; }
    
    // Import once, use in multiple files
    import type { Pagination } from "../types/shared";
    
  • Compose types with intersection to combine existing shapes without duplication.

    type Audited = { createdBy: string; updatedBy: string };
    type Product = { id: number; name: string };
    type AuditedProduct = Product & Audited;
    
  • Use mapped types to derive Partial and Readonly variants from one source interface.

    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 extremely fast bundling — it transpiles TypeScript without type checking.

    npm install -D esbuild
    esbuild src/index.ts --bundle --outfile=dist/index.js
    
  • Set skipLibCheck to skip type-checking of node_modules declaration files.

    {
      "compilerOptions": {
        "skipLibCheck": true
      }
    }
    
  • Use ts-loader with transpileOnly to skip type checking during webpack builds.

    {
      loader: 'ts-loader',
      options: {
        transpileOnly: true,       // skips type checking, much faster
        experimentalWatchApi: true
      }
    }
    
  • Use SWC to transpile fast, then run tsc separately for type checking only.

    # Transpile fast with SWC, check types in a separate step
    swc src -d dist
    tsc --noEmit  # type checking only, no JS output
    
  • Use project references to split monorepos into independently cached packages.

    {
      "references": [
        { "path": "./packages/core" },
        { "path": "./packages/ui" }
      ],
      "compilerOptions": { "composite": true }
    }
    

FAQ