TypeScript Performance Tips
Accelerate TypeScript compilation by profiling diagnostics, optimizing recursive types, isolating modules, and using project references.
TL;DR
- Profile compilation bottlenecks using the
tsc --diagnosticsreporting flag. - Eliminate runtime import overhead by adopting explicit
import typesyntax. - Accelerate rebuilds across monorepos using composite
projectReferencesbuild configurations.
Compiler Diagnostics and Profiling
Basic diagnostic metricsDisplay time spent in each compilation phase.
# Display summary compilation statistics
tsc --diagnosticsExtended diagnostic reportOutput memory usage and detailed I/O breakdown.
# Print detailed timing and memory metrics
tsc --extendedDiagnosticsChrome trace generationGenerate timeline traces for Chrome DevTools.
# Output trace files into ./trace-dir
tsc --generateTrace ./trace-dirIncremental build cachingCache build state to accelerate re-runs.
# tsconfig compilerOptions:
# { "incremental": true }
tsc --buildType Definition Optimization
Intermediate type aliasesDecompose complex generics to enable caching.
// Bad: deeply nested inline evaluation
// type Nested = Promise<Record<string, Array<User>>>;
type UserList = Array<string>;
type UserRecord = Record<string, UserList>;
type ResultPromise = Promise<UserRecord>;Prefer interfaces for shapesUse interface declarations for caching benefits.
// Interfaces are cached by compiler:
interface UserProfile {
id: string;
name: string;
}Narrow union sizesPartition large unions into distinct subtypes.
type SuccessCode = 200 | 201 | 204;
type ErrorCode = 400 | 404 | 500;
type HttpCode = SuccessCode | ErrorCode;Const assertions over wideningFreeze literals without runtime overhead.
const HTTP_METHODS = [
"GET",
"POST",
"DELETE"
] as const;
type Method = typeof HTTP_METHODS[number];Type-Only Imports and Syntax
Dedicated type importsImport types that are fully erased during emit.
import type { Request, Response } from "express";
import express from "express";Inline type import specifierMix value and type imports cleanly.
import {
readFile,
type FileHandle
} from "node:fs/promises";Verbatim module syntaxEnforce type import elision rules.
// In tsconfig.json compilerOptions:
// { "verbatimModuleSyntax": true }
import type { Config } from "./config";Type-only re-exportsExport types without importing runtime code.
export type { UserProfile } from "./user";
export type { AccountSettings } from "./account";Structural Sharing and Deduplication
Base interface extensionShare common properties across model types.
interface Entity {
id: string;
createdAt: number;
}
interface Article extends Entity {
title: string;
}Declaration mergingExtend global interfaces cleanly.
interface Window {
analyticsTracker?: (name: string) => void;
}Utility mapped derivationsDerive variants from single source of truth.
interface Todo {
title: string;
completed: boolean;
}
type TodoUpdate = Partial<Todo>;
type ReadonlyTodo = Readonly<Todo>;Discriminated unions over genericsSimplify compiler checks with discriminant tags.
type TaskState =
| { state: "idle" }
| { state: "loading" }
| { state: "done"; data: string };Build Tool and Project Architecture
Skip external lib checkSkip type checking on node_modules d.ts.
{
"compilerOptions": {
"skipLibCheck": true
}
}Isolated modules enforcementEnsure each file transpiles independently.
{
"compilerOptions": {
"isolatedModules": true
}
}Project referencesSplit monorepos into incremental sub-projects.
{
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/api" }
]
}Composite package flagEnable project reference emission contracts.
{
"compilerOptions": {
"composite": true,
"declaration": true
}
}Tips
- Enable
skipLibCheckin compiler options to bypass type verification of external declaration files and immediately cut build times in half. - Prefer
interfaceover type aliases for object definitions, as the compiler caches interface declarations much more efficiently during type checking.
Warnings
- Avoid deeply nested recursive generic types like
DeepPartial, which can cause exponential compiler expansion and catastrophic memory exhaustion. - Omitting
isolatedModuleswhen using non-tsc transpilers like esbuild can lead to silent runtime crashes on const enums and re-exports.
In Practice
Configuring build tools and refactoring slow recursive types to achieve instantaneous incremental compilation.
- Extract intermediate interfaces to leverage compiler type caching.
- Replace runtime imports with erased type-only imports.
- Enable compiler flags for incremental caching and skipped checks.
- Verify compilation speed improvement with diagnostics flags.
import type { IncomingMessage } from "node:http";
interface BasePayload {
timestamp: number;
origin: string;
}
interface UserPayload extends BasePayload {
userId: string;
action: "login" | "logout";
}
export function parse(req: IncomingMessage, raw: any) {
return {
timestamp: Date.now(),
origin: req.url ?? "unknown",
userId: (raw as UserPayload).userId,
action: (raw as UserPayload).action
};
}FAQ
Run tsc --diagnostics or tsc --extendedDiagnostics to view a breakdown of time spent across check, parse, and emit phases. For micro-benchmarks, append --generateTrace trace-dir and inspect the generated trace in Chrome DevTools to locate slow types.
The import type syntax guarantees that imported symbols are erased completely during JavaScript emission. This prevents accidental runtime dependencies, eliminates side-effect evaluations, and reduces the work needed by bundlers.
Deeply nested or recursive conditional types force the compiler to recursively evaluate permutations of shapes, triggering exponential branching. Breaking complex generics into named intermediate type aliases provides caching checkpoints that drastically reduce compile cycles.
Project references configured with composite: true enable tsc --build to compile individual packages incrementally. TypeScript caches .tsbuildinfo outputs, so unchanged projects are skipped entirely during subsequent builds.
Setting skipLibCheck: true skips re-verifying .d.ts declaration files inside node_modules. Because external packages are already validated prior to release, this provides a massive compilation speedup while your project source code remains strictly type-checked.