Technology · TypeScript
TypeScript Performance Tips
Profile code, optimize types, and identify performance bottlenecks in typed applications.
TL;DR
- 01Profile TypeScript compilation with diagnostics flags.
- 02Avoid deeply nested generic types and large union types.
- 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.