TypeScript Performance Tips
Profile code, optimize types, and identify performance bottlenecks in typed applications.
TL;DR
- 01Profile build time using tsc --diagnostics and --generateTrace flags.
- 02Avoid deeply nested generic types and large union types.
- 03Use import type to eliminate runtime overhead in compiled output.
Tips
- 01Profile your TypeScript compilation regularly to catch performance regressions early — small type optimizations add up.
Warnings
- 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 timeGenerate a trace file and open it in Chrome for deep performance analysis.
tsc --generateTrace ./trace # Open chrome://tracing and load ./trace/trace.jsonEnable 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 changesUse 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 emitEnable 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 neededExtend 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.jsSet 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 outputUse project references to split monorepos into independently cached packages.
{ "references": [ { "path": "./packages/core" }, { "path": "./packages/ui" } ], "compilerOptions": { "composite": true } }
FAQ
Run tsc --diagnostics or tsc --extendedDiagnostics to get a breakdown of time spent on each compilation phase. For deeper analysis, use --generateTrace to produce a trace file you can load in Chrome DevTools.
Type-only imports (import type { Foo } from './foo') are erased entirely at emit time, so they never appear in compiled JavaScript. This reduces bundle size and prevents accidental runtime dependencies on modules that only contain types.
Deeply nested generics force the compiler to recursively instantiate and check complex type structures, which can exponentially increase type-checking time. Flatten or break them into intermediate named type aliases to give the compiler checkpoints and reduce redundant work.
Use project references (composite: true with tsc --build) so each package is compiled incrementally and results are cached. This avoids re-checking unchanged packages on every build and enables parallel compilation across projects.
skipLibCheck: true skips type-checking of .d.ts files in node_modules, which can meaningfully cut build time in large projects. It's a safe tradeoff in most cases since library types are typically pre-validated — just ensure your own source files still get full checking.