TypeScript Performance Tips

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

TL;DR

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

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 }
    }
    

Tips

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

Warnings

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

FAQ