TypeScript Performance Optimization: 5 Production Patterns from Compile-Time to Runtime
Your TypeScript Project Is Dying a Slow Death
A 100K-line TypeScript project takes 45 seconds for tsc, HMR refreshes lag for 3 seconds, the bundle is 2.4MB, and the production first paint takes 3.8 seconds — this isn't an outlier, it's the reality for most TypeScript projects in 2026. The type system was supposed to be a developer productivity multiplier, but it's become a performance killer. Nobody configures Project References, nobody dares enable skipLibCheck, generics nest five layers deep, runtime type guards sit on hot paths, and the bundle is stuffed with dead code that survived tree-shaking.
This article starts from core concepts and guides you through type checking acceleration → build optimization → runtime performance → memory efficiency → production monitoring with 5 production patterns, from compile-time to runtime, from developer experience to production metrics.
Core Concepts
| Concept | Description |
|---|---|
| Type Checking Speed | Speed at which the TypeScript compiler checks types, affected by project structure, type complexity, and configuration |
| Project References | Splitting large projects into multiple sub-projects for incremental compilation and parallel type checking |
| Incremental Build | Using caches to re-check only changed portions, avoiding full type checking |
| Tree-Shaking | Build tools identifying and removing unreferenced code to reduce final bundle size |
| Structural Typing | TypeScript's structural type system, determining type compatibility by shape rather than by name |
| Branded Types | Creating nominal types through unique markers, achieving type isolation within a structural type system |
| Const Assertions | as const assertions inferring values as narrowest literal types, reducing type space bloat |
| Performance Budget | Setting budget thresholds for bundle size, compilation time, etc., automatically blocking performance regressions in CI |
Optimization Flow
Compile-time optimization:
Project splitting (Project References) → Incremental build → skipLibCheck → Isolated module compilation
Build-time optimization:
tsup/esbuild replacing tsc → Tree-Shaking → Code Splitting → Bundle compression
Runtime optimization:
Avoid hot-path type guards → Efficient generics → Inline type assertions → Reduce runtime type checking
Memory optimization:
Structural type reuse → Branded Types replacing class inheritance → const assertions → Type narrowing
Production monitoring:
Type coverage → Bundle analysis → Performance Budget → CI integration
Problem Analysis: 5 Major TypeScript Performance Bottlenecks
- Slow type checking speed: A single large project's full tsc time grows linearly with code volume; a 100K-line project needs 30-60 seconds for tsc, blocking CI pipelines
- Bloated build output: tsc only does type checking without tree-shaking; barrel exports cause massive dead code in bundles; runtime type guard code bloat
- Runtime performance overhead: Frequent
typeof/instanceofchecks on hot paths, runtime overhead from deep generic instantiation, excessive class decorator usage - Poor memory efficiency: Complex type inference consumes massive compiler memory, runtime creation of duplicate objects for similar structural types, unused const assertions causing type space bloat
- Lack of performance monitoring: No type coverage metrics, no budget control for bundle size, no awareness of compilation time regression, inability to trace production performance issues back to type design
Step-by-Step: 5 Production Patterns
Pattern 1: Type Checking Speed Optimization
// tsconfig.base.json - Base configuration
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true
}
}
// packages/core/tsconfig.json - Core package configuration
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"],
"references": [
{ "path": "../shared" }
]
}
// packages/shared/tsconfig.json - Shared package configuration
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
// packages/app/tsconfig.json - Application package configuration
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"noEmit": true
},
"include": ["src"],
"references": [
{ "path": "../core" },
{ "path": "../shared" }
]
}
// tsconfig.json - Top-level solution configuration
{
"files": [],
"references": [
{ "path": "packages/shared" },
{ "path": "packages/core" },
{ "path": "packages/app" }
]
}
// scripts/typecheck.ts - Incremental type checking script
import { execSync } from 'child_process';
import { readFileSync, existsSync, unlinkSync } from 'fs';
import { performance } from 'perf_hooks';
interface TypeCheckResult {
project: string;
duration: number;
errors: number;
cached: boolean;
}
function typecheckProject(projectPath: string, clean: boolean = false): TypeCheckResult {
const buildInfoFile = `${projectPath}/.tsbuildinfo`;
const wasCached = existsSync(buildInfoFile) && !clean;
if (clean && existsSync(buildInfoFile)) {
unlinkSync(buildInfoFile);
}
const start = performance.now();
let errors = 0;
try {
execSync(`npx tsc --build ${projectPath} --verbose`, {
encoding: 'utf-8',
stdio: 'pipe',
});
} catch (error: any) {
const output = error.stdout || error.stderr || '';
const errorMatches = output.match(/error TS\d+:/g);
errors = errorMatches ? errorMatches.length : 0;
}
const duration = performance.now() - start;
return {
project: projectPath,
duration: Math.round(duration),
errors,
cached: wasCached,
};
}
function runFullTypeCheck(clean: boolean = false): void {
const projects = [
'packages/shared',
'packages/core',
'packages/app',
];
console.log('=== TypeScript Type Check Report ===\n');
let totalDuration = 0;
let totalErrors = 0;
for (const project of projects) {
const result = typecheckProject(project, clean);
totalDuration += result.duration;
totalErrors += result.errors;
const status = result.errors === 0 ? '✓' : '✗';
const cacheStatus = result.cached ? '(cached)' : '(full)';
console.log(
`${status} ${result.project}: ${result.duration}ms ${cacheStatus}, ${result.errors} errors`
);
}
console.log(`\nTotal: ${totalDuration}ms, ${totalErrors} errors`);
if (totalDuration > 10000) {
console.warn('\n⚠️ Type check exceeds 10s budget! Consider:');
console.warn(' - Adding more project references');
console.warn(' - Enabling skipLibCheck');
console.warn(' - Using isolatedModules');
}
}
const isClean = process.argv.includes('--clean');
runFullTypeCheck(isClean);
// src/types/performance-optimized.ts - Optimized type definitions to reduce checking burden
// ❌ Complex conditional types cause slow type checking
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
type DeepRequired<T> = T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T;
// ✅ Use built-in utility types, avoid repeated instantiation
type CachedPartial<T> = Partial<T>;
type CachedRequired<T> = Required<T>;
// ❌ Excessive recursive type depth
type FlattenDeep<T> = T extends Array<infer U>
? FlattenDeep<U>
: T;
// ✅ Limit recursion depth
type FlattenN<T, N extends number, D extends any[] = []> =
D['length'] extends N
? T
: T extends Array<infer U>
? FlattenN<U, N, [...D, 1]>
: T;
type Flatten3<T> = FlattenN<T, 3>;
// ❌ Overuse of template literal types
type Route = `/api/${string}/${string}`;
// ✅ Use union types instead of template literals
type ApiRoute =
| '/api/users'
| '/api/users/:id'
| '/api/posts'
| '/api/posts/:id'
| '/api/comments'
| '/api/comments/:id';
// ❌ Large union types
type Status = 'pending' | 'processing' | 'approved' | 'rejected' | 'cancelled' | 'refunded' | 'completed' | 'archived' | 'draft' | 'published';
// ✅ Grouped union types
type OrderStatus = 'pending' | 'processing' | 'completed' | 'cancelled';
type ContentStatus = 'draft' | 'published' | 'archived';
type PaymentStatus = 'approved' | 'rejected' | 'refunded';
// skipLibCheck-optimized .d.ts
declare module 'heavy-lib' {
export interface HeavyConfig {
readonly apiKey: string;
readonly endpoint: string;
readonly timeout: number;
readonly retries: number;
}
export function createClient(config: HeavyConfig): HeavyClient;
export interface HeavyClient {
getData<T>(id: string): Promise<T>;
setData<T>(id: string, data: T): Promise<void>;
}
}
Pattern 2: Build Optimization with tsup/esbuild
// tsup.config.ts - tsup build configuration
import { defineConfig } from 'tsup';
export default defineConfig([
{
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: {
resolve: true,
compilerOptions: {
skipLibCheck: true,
composite: false,
},
},
splitting: true,
treeshake: true,
minify: true,
sourcemap: true,
clean: true,
outDir: 'dist',
target: 'es2022',
platform: 'node',
external: ['react', 'react-dom'],
esbuildOptions(options) {
options.logLevel = 'info';
options.chunkNames = 'chunks/[name]-[hash]';
},
},
{
entry: ['src/browser.ts'],
format: ['esm'],
treeshake: true,
minify: true,
sourcemap: true,
outDir: 'dist/browser',
target: 'es2022',
platform: 'browser',
esbuildOptions(options) {
options.conditions = ['browser'];
},
},
]);
// src/index.ts - Optimized export structure for tree-shaking
// ❌ barrel exports break tree-shaking
// export * from './utils';
// export * from './validators';
// export * from './transformers';
// ✅ Explicit named exports for bundler dependency analysis
export { validateEmail, validateUrl } from './validators/email';
export { validateAge, validateRange } from './validators/number';
export { formatDate, parseDate } from './transformers/date';
export { formatCurrency, parseCurrency } from './transformers/currency';
// ✅ Use conditional exports to reduce browser bundle
export type { UserSchema, CreateUserInput } from './types/user';
export type { PaginationParams, SearchParams } from './types/pagination';
// scripts/bundle-analysis.ts - Bundle analysis script
import { build } from 'esbuild';
import { readFileSync, writeFileSync } from 'fs';
import { gzipSync } from 'zlib';
interface BundleMetrics {
name: string;
size: number;
gzipSize: number;
modules: number;
}
const BUDGET = {
maxBundleSize: 100 * 1024,
maxGzipSize: 30 * 1024,
maxChunkSize: 50 * 1024,
};
async function analyzeBundle(entryPoint: string): Promise<BundleMetrics> {
const result = await build({
entryPoints: [entryPoint],
bundle: true,
minify: true,
write: false,
metafile: true,
format: 'esm',
target: 'es2022',
external: ['react', 'react-dom'],
});
const outputFiles = result.outputFiles || [];
const metafile = result.metafile;
let totalSize = 0;
for (const file of outputFiles) {
totalSize += file.contents.byteLength;
}
const gzipSize = gzipSync(Buffer.from(outputFiles[0]?.contents || [])).byteLength;
const moduleCount = metafile ? Object.keys(metafile.inputs).length : 0;
return {
name: entryPoint,
size: totalSize,
gzipSize,
modules: moduleCount,
};
}
async function runBundleAnalysis(): Promise<void> {
console.log('=== Bundle Analysis Report ===\n');
const entryPoints = [
'src/index.ts',
'src/browser.ts',
];
let hasViolation = false;
for (const entry of entryPoints) {
const metrics = await analyzeBundle(entry);
const sizeKB = (metrics.size / 1024).toFixed(1);
const gzipKB = (metrics.gzipSize / 1024).toFixed(1);
console.log(`${entry}:`);
console.log(` Size: ${sizeKB}KB (budget: ${BUDGET.maxBundleSize / 1024}KB)`);
console.log(` Gzip: ${gzipKB}KB (budget: ${BUDGET.maxGzipSize / 1024}KB)`);
console.log(` Modules: ${metrics.modules}`);
if (metrics.size > BUDGET.maxBundleSize) {
console.warn(` ⚠️ Bundle size exceeds budget!`);
hasViolation = true;
}
if (metrics.gzipSize > BUDGET.maxGzipSize) {
console.warn(` ⚠️ Gzip size exceeds budget!`);
hasViolation = true;
}
console.log();
}
if (hasViolation) {
console.error('❌ Performance budget violated!');
process.exit(1);
} else {
console.log('✅ All bundles within budget.');
}
}
runBundleAnalysis();
// package.json - Optimized build scripts
{
"scripts": {
"typecheck": "tsc --build",
"typecheck:clean": "tsc --build --clean && tsc --build",
"build": "tsup",
"build:analyze": "tsup --metafile && node scripts/bundle-analysis.ts",
"dev": "tsup --watch",
"check:all": "npm run typecheck && npm run build && npm run test",
"prepack": "npm run build"
}
}
Pattern 3: Runtime Performance Optimization
// src/runtime/hot-path.ts - Hot path type guard optimization
// ❌ Complex type guards on hot paths
function processData(data: unknown): ProcessedData {
if (
typeof data === 'object' &&
data !== null &&
'id' in data &&
typeof (data as any).id === 'string' &&
'name' in data &&
typeof (data as any).name === 'string' &&
'age' in data &&
typeof (data as any).age === 'number' &&
'email' in data &&
typeof (data as any).email === 'string'
) {
return data as ProcessedData;
}
throw new Error('Invalid data');
}
// ✅ Minimized type guard, trusting compile-time types
interface ProcessedData {
id: string;
name: string;
age: number;
email: string;
}
function processDataOptimized(data: unknown): ProcessedData {
if (typeof data !== 'object' || data === null) {
throw new Error('Expected object');
}
return data as ProcessedData;
}
// ✅ Layered validation: fast check on hot paths, strict validation on cold paths
function processHotPath(data: unknown): ProcessedData {
if (typeof data !== 'object' || data === null) {
throw new Error('Expected object');
}
return data as ProcessedData;
}
function processColdPath(data: unknown): ProcessedData {
const result = ProcessedDataSchema.safeParse(data);
if (!result.success) {
throw new Error(`Validation failed: ${result.error.message}`);
}
return result.data;
}
import { z } from 'zod';
const ProcessedDataSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
age: z.number().int().min(0),
email: z.string().email(),
});
// src/runtime/generics.ts - Efficient generic patterns
// ❌ Generic class instantiation causes runtime bloat
class DataStore<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
get(index: number): T | undefined {
return this.items[index];
}
filter(predicate: (item: T) => boolean): T[] {
return this.items.filter(predicate);
}
map<U>(transform: (item: T) => U): U[] {
return this.items.map(transform);
}
reduce<U>(reducer: (acc: U, item: T) => U, initial: U): U {
return this.items.reduce(reducer, initial);
}
}
// ✅ Use functional utilities instead of generic classes
interface DataStore {
readonly items: readonly unknown[];
}
function createStore<T>(initialItems: T[] = []): DataStore {
return { items: Object.freeze([...initialItems]) };
}
function addItem<T>(store: DataStore, item: T): DataStore {
return { items: Object.freeze([...store.items, item]) };
}
function getItem<T>(store: DataStore, index: number): T {
return store.items[index] as T;
}
function filterItems<T>(store: DataStore, predicate: (item: T) => boolean): T[] {
return store.items.filter(predicate as (item: unknown) => boolean) as T[];
}
// ✅ Use conditional types instead of runtime branching
type Result<T, E = Error> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: E };
function ok<T>(value: T): Result<T> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
function mapResult<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> {
return result.ok ? ok(fn(result.value)) : result;
}
function flatMapResult<T, U, E>(
result: Result<T, E>,
fn: (value: T) => Result<U, E>
): Result<U, E> {
return result.ok ? fn(result.value) : result;
}
// ✅ Avoid runtime type erasure overhead
interface TypeTag<T extends string> {
readonly __type: T;
}
type UserId = string & TypeTag<'UserId'>;
type OrderId = string & TypeTag<'OrderId'>;
function createUserId(id: string): UserId {
return id as UserId;
}
function createOrderId(id: string): OrderId {
return id as OrderId;
}
function getUser(id: UserId): Promise<User | null> {
return Promise.resolve(null);
}
function getOrder(id: OrderId): Promise<Order | null> {
return Promise.resolve(null);
}
interface User {
id: UserId;
name: string;
}
interface Order {
id: OrderId;
total: number;
}
// src/runtime/iteration.ts - Efficient iteration patterns
// ❌ Creating new arrays on each iteration
function processUsers(users: User[]): ProcessedUser[] {
return users
.filter((u) => u.age >= 18)
.map((u) => ({ ...u, displayName: u.name.toUpperCase() }))
.filter((u) => u.displayName.length > 0)
.map((u) => ({ ...u, score: calculateScore(u) }));
}
// ✅ Single-pass replaces multiple chained calls
function processUsersOptimized(users: User[]): ProcessedUser[] {
const result: ProcessedUser[] = [];
for (const u of users) {
if (u.age < 18) continue;
const displayName = u.name.toUpperCase();
if (displayName.length === 0) continue;
result.push({
...u,
displayName,
score: calculateScore(u),
});
}
return result;
}
// ✅ Use generators for lazy computation
function* filterUsers(users: Iterable<User>): Generator<User> {
for (const u of users) {
if (u.age >= 18) yield u;
}
}
function* mapUsers(users: Iterable<User>): Generator<ProcessedUser> {
for (const u of users) {
yield {
...u,
displayName: u.name.toUpperCase(),
score: calculateScore(u),
};
}
}
function processUsersLazy(users: User[]): ProcessedUser[] {
return [...mapUsers(filterUsers(users))];
}
interface ProcessedUser extends User {
displayName: string;
score: number;
}
function calculateScore(user: User): number {
return user.name.length * user.age;
}
Pattern 4: Memory Efficiency Optimization
// src/memory/structural-typing.ts - Structural type reuse
// ❌ Each interface defined independently, compiler creates separate types
interface UserResponse {
id: string;
name: string;
email: string;
role: 'admin' | 'editor' | 'viewer';
createdAt: string;
}
interface UserListItem {
id: string;
name: string;
email: string;
role: 'admin' | 'editor' | 'viewer';
createdAt: string;
}
interface UserProfile {
id: string;
name: string;
email: string;
role: 'admin' | 'editor' | 'viewer';
createdAt: string;
bio: string;
avatar: string;
}
// ✅ Use base types + composition for reuse
interface BaseEntity {
readonly id: string;
readonly createdAt: string;
readonly updatedAt: string;
}
type UserRole = 'admin' | 'editor' | 'viewer';
interface UserBase extends BaseEntity {
readonly name: string;
readonly email: string;
readonly role: UserRole;
}
type UserResponse = UserBase;
type UserListItem = UserBase;
interface UserProfile extends UserBase {
readonly bio: string;
readonly avatar: string;
}
// ✅ Use Pick/Omit to derive from base types
type UserSummary = Pick<UserBase, 'id' | 'name' | 'role'>;
type UserEmailInfo = Pick<UserBase, 'id' | 'email'>;
type UserWithoutRole = Omit<UserBase, 'role'>;
// src/memory/branded-types.ts - Branded Types replacing class inheritance
// ❌ Class inheritance causes runtime prototype chain overhead
class Entity {
constructor(
public id: string,
public createdAt: Date,
) {}
}
class UserEntity extends Entity {
constructor(
id: string,
createdAt: Date,
public name: string,
public email: string,
) {
super(id, createdAt);
}
}
class OrderEntity extends Entity {
constructor(
id: string,
createdAt: Date,
public total: number,
public status: string,
) {
super(id, createdAt);
}
}
// ✅ Use Branded Types + pure data objects
interface Brand<T extends string> {
readonly __brand: T;
}
type Branded<T, B extends string> = T & Brand<B>;
type UserId = Branded<string, 'UserId'>;
type OrderId = Branded<string, 'OrderId'>;
type ProductId = Branded<string, 'ProductId'>;
function createUserId(id: string): UserId {
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id)) {
throw new Error('Invalid UUID format for UserId');
}
return id as UserId;
}
function createOrderId(id: string): OrderId {
if (!/^ORD-\d{8}$/.test(id)) {
throw new Error('Invalid order ID format');
}
return id as OrderId;
}
interface User {
readonly id: UserId;
readonly name: string;
readonly email: string;
readonly role: UserRole;
}
interface Order {
readonly id: OrderId;
readonly userId: UserId;
readonly total: number;
readonly status: OrderStatus;
readonly items: ReadonlyArray<OrderItem>;
}
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
interface OrderItem {
readonly productId: ProductId;
readonly quantity: number;
readonly unitPrice: number;
}
// ✅ Branded Types ensure type safety with zero runtime overhead
function getUserOrders(userId: UserId, orders: ReadonlyArray<Order>): ReadonlyArray<Order> {
return orders.filter((o) => o.userId === userId);
}
// Compile-time prevention of ID misuse
// getUserOrders(createOrderId('ORD-20260616'), orders); // ❌ Type error
// src/memory/const-assertions.ts - const assertions optimization
// ❌ Broad types cause compiler to create many type instances
const ROUTES = {
users: '/api/users',
posts: '/api/posts',
comments: '/api/comments',
};
type RouteKey = keyof typeof ROUTES; // string
type RouteValue = typeof ROUTES[keyof typeof ROUTES]; // string
// ✅ const assertion locks literal types
const ROUTES = {
users: '/api/users',
posts: '/api/posts',
comments: '/api/comments',
} as const;
type RouteKey = keyof typeof ROUTES; // 'users' | 'posts' | 'comments'
type RouteValue = typeof ROUTES[keyof typeof ROUTES]; // '/api/users' | '/api/posts' | '/api/comments'
// ✅ const assertion + satisfies pattern
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const;
type HttpMethod = typeof HTTP_METHODS[number]; // 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
const STATUS_CODES = {
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
INTERNAL_ERROR: 500,
} as const;
type StatusCode = typeof STATUS_CODES[keyof typeof STATUS_CODES];
// 200 | 201 | 400 | 401 | 403 | 404 | 500
// ✅ Use satisfies to ensure correct types while preserving literal types
const API_CONFIG = {
baseUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
} as const satisfies Record<string, string | number | Record<string, string>>;
type ApiConfigKey = keyof typeof API_CONFIG; // 'baseUrl' | 'timeout' | 'retries' | 'headers'
// ✅ Freeze objects to prevent runtime modification
const IMMUTABLE_CONFIG = Object.freeze({
MAX_RETRIES: 3,
TIMEOUT_MS: 5000,
PAGE_SIZE: 20,
MAX_PAGE_SIZE: 100,
} as const);
type Config = typeof IMMUTABLE_CONFIG;
// src/memory/type-narrowing.ts - Efficient type narrowing
// ❌ Repeated type guards
function handleData(data: unknown): string {
if (typeof data === 'object' && data !== null && 'type' in data) {
if ((data as any).type === 'user') {
if (typeof (data as any).name === 'string') {
return (data as any).name;
}
}
if ((data as any).type === 'order') {
if (typeof (data as any).id === 'string') {
return (data as any).id;
}
}
}
return 'unknown';
}
// ✅ Use discriminated unions + type guard functions
interface UserData {
readonly type: 'user';
readonly name: string;
readonly email: string;
}
interface OrderData {
readonly type: 'order';
readonly id: string;
readonly total: number;
}
type AppData = UserData | OrderData;
function isUserData(data: AppData): data is UserData {
return data.type === 'user';
}
function isOrderData(data: AppData): data is OrderData {
return data.type === 'order';
}
function handleDataOptimized(data: AppData): string {
switch (data.type) {
case 'user':
return data.name;
case 'order':
return data.id;
}
}
// ✅ Use type predicate caching
type TypeGuard<T> = (value: unknown) => value is T;
function createTypeGuard<T>(check: (value: unknown) => boolean): TypeGuard<T> {
return (value: unknown): value is T => check(value);
}
const isString = createTypeGuard<string>((v) => typeof v === 'string');
const isNumber = createTypeGuard<number>((v) => typeof v === 'number');
const isBoolean = createTypeGuard<boolean>((v) => typeof v === 'boolean');
function parseEnvValue<T>(
value: string | undefined,
guard: TypeGuard<T>,
defaultValue: T,
): T {
if (value === undefined) return defaultValue;
const parsed: unknown = value;
return guard(parsed) ? parsed : defaultValue;
}
const port = parseEnvValue(process.env.PORT, isNumber, 3000);
const debug = parseEnvValue(process.env.DEBUG, isBoolean, false);
Pattern 5: Production Monitoring and Performance Budgets
// scripts/type-coverage.ts - Type coverage checking
import { execSync } from 'child_process';
interface TypeCoverageResult {
totalFiles: number;
typedFiles: number;
coverage: number;
anyCount: number;
unsafeCount: number;
}
function checkTypeCoverage(): TypeCoverageResult {
try {
const output = execSync(
'npx type-coverage --detail --at-least 95',
{ encoding: 'utf-8', stdio: 'pipe' }
);
const match = output.match(/(\d+)\/(\d+) files/);
const anyMatch = output.match(/(\d+) any/);
const typedFiles = match ? parseInt(match[1], 10) : 0;
const totalFiles = match ? parseInt(match[2], 10) : 0;
const anyCount = anyMatch ? parseInt(anyMatch[1], 10) : 0;
return {
totalFiles,
typedFiles,
coverage: totalFiles > 0 ? (typedFiles / totalFiles) * 100 : 0,
anyCount,
unsafeCount: anyCount,
};
} catch {
return {
totalFiles: 0,
typedFiles: 0,
coverage: 0,
anyCount: -1,
unsafeCount: -1,
};
}
}
function reportTypeCoverage(): void {
const result = checkTypeCoverage();
console.log('=== Type Coverage Report ===\n');
console.log(`Files: ${result.typedFiles}/${result.totalFiles}`);
console.log(`Coverage: ${result.coverage.toFixed(1)}%`);
console.log(`Any count: ${result.anyCount}`);
if (result.coverage < 95) {
console.warn('\n⚠️ Type coverage below 95%!');
console.warn(' Run `npx type-coverage --detail` to find untyped code.');
}
if (result.coverage < 80) {
console.error('❌ Type coverage critically low!');
process.exit(1);
}
}
reportTypeCoverage();
// scripts/performance-budget.ts - Performance budget CI check
import { readFileSync } from 'fs';
import { gzipSync } from 'zlib';
interface PerformanceBudget {
maxBundleSize: number;
maxGzipSize: number;
maxTypeCheckTime: number;
minTypeCoverage: number;
maxChunkCount: number;
}
const DEFAULT_BUDGET: PerformanceBudget = {
maxBundleSize: 100 * 1024, // 100KB
maxGzipSize: 30 * 1024, // 30KB
maxTypeCheckTime: 15000, // 15s
minTypeCoverage: 95, // 95%
maxChunkCount: 10,
};
interface BudgetCheckResult {
metric: string;
value: number;
budget: number;
unit: string;
passed: boolean;
}
function checkBundleBudget(
bundlePath: string,
budget: PerformanceBudget = DEFAULT_BUDGET,
): BudgetCheckResult[] {
const results: BudgetCheckResult[] = [];
try {
const content = readFileSync(bundlePath);
const size = content.byteLength;
const gzipSize = gzipSync(content).byteLength;
results.push({
metric: 'Bundle Size',
value: size,
budget: budget.maxBundleSize,
unit: 'bytes',
passed: size <= budget.maxBundleSize,
});
results.push({
metric: 'Gzip Size',
value: gzipSize,
budget: budget.maxGzipSize,
unit: 'bytes',
passed: gzipSize <= budget.maxGzipSize,
});
} catch {
results.push({
metric: 'Bundle Size',
value: -1,
budget: budget.maxBundleSize,
unit: 'bytes',
passed: false,
});
}
return results;
}
function checkTypeCheckBudget(
budget: PerformanceBudget = DEFAULT_BUDGET,
): BudgetCheckResult {
const start = Date.now();
try {
execSync('npx tsc --noEmit', { encoding: 'utf-8', stdio: 'pipe' });
} catch {
// type errors are handled separately
}
const duration = Date.now() - start;
return {
metric: 'Type Check Time',
value: duration,
budget: budget.maxTypeCheckTime,
unit: 'ms',
passed: duration <= budget.maxTypeCheckTime,
};
}
function runPerformanceBudget(): void {
console.log('=== Performance Budget Check ===\n');
const results: BudgetCheckResult[] = [
...checkBundleBudget('dist/index.js'),
checkTypeCheckBudget(),
];
let allPassed = true;
for (const result of results) {
const status = result.passed ? '✓' : '✗';
const valueStr = result.value >= 0
? result.unit === 'bytes'
? `${(result.value / 1024).toFixed(1)}KB`
: `${result.value}${result.unit}`
: 'N/A';
const budgetStr = result.unit === 'bytes'
? `${(result.budget / 1024).toFixed(1)}KB`
: `${result.budget}${result.unit}`;
console.log(`${status} ${result.metric}: ${valueStr} (budget: ${budgetStr})`);
if (!result.passed) {
allPassed = false;
}
}
if (!allPassed) {
console.error('\n❌ Performance budget violated!');
process.exit(1);
} else {
console.log('\n✅ All performance budgets met.');
}
}
import { execSync } from 'child_process';
runPerformanceBudget();
// .github/workflows/typescript-perf.yml - CI performance monitoring
// GitHub Actions workflow configuration
/*
name: TypeScript Performance
on:
pull_request:
paths:
- '**.ts'
- '**.tsx'
- 'tsconfig*.json'
- 'package.json'
jobs:
performance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Type Check
run: |
START=$(date +%s%N)
npx tsc --noEmit
END=$(date +%s%N)
DURATION=$(( (END - START) / 1000000 ))
echo "Type check duration: ${DURATION}ms"
if [ $DURATION -gt 15000 ]; then
echo "::error::Type check exceeds 15s budget (${DURATION}ms)"
exit 1
fi
- name: Type Coverage
run: |
npx type-coverage --at-least 95 || {
echo "::error::Type coverage below 95%"
exit 1
}
- name: Bundle Analysis
run: |
npm run build
node scripts/performance-budget.ts
- name: Comment PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const report = fs.readFileSync('perf-report.txt', 'utf8');
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## 📊 TypeScript Performance Report\n${report}`
});
*/
// src/monitoring/runtime-metrics.ts - Runtime performance metric collection
interface PerformanceMetric {
name: string;
value: number;
timestamp: number;
tags: Record<string, string>;
}
class PerformanceCollector {
private metrics: PerformanceMetric[] = [];
private readonly maxMetrics = 1000;
recordMetric(name: string, value: number, tags: Record<string, string> = {}): void {
this.metrics.push({
name,
value,
timestamp: Date.now(),
tags,
});
if (this.metrics.length > this.maxMetrics) {
this.metrics = this.metrics.slice(-this.maxMetrics);
}
}
timeAsync<T>(name: string, fn: () => Promise<T>, tags?: Record<string, string>): Promise<T> {
const start = performance.now();
return fn().finally(() => {
const duration = performance.now() - start;
this.recordMetric(name, duration, { ...tags, unit: 'ms' });
});
}
timeSync<T>(name: string, fn: () => T, tags?: Record<string, string>): T {
const start = performance.now();
try {
return fn();
} finally {
const duration = performance.now() - start;
this.recordMetric(name, duration, { ...tags, unit: 'ms' });
}
}
getMetrics(name?: string): PerformanceMetric[] {
if (name) {
return this.metrics.filter((m) => m.name === name);
}
return [...this.metrics];
}
getAverage(name: string): number {
const named = this.getMetrics(name);
if (named.length === 0) return 0;
return named.reduce((sum, m) => sum + m.value, 0) / named.length;
}
getP95(name: string): number {
const named = this.getMetrics(name);
if (named.length === 0) return 0;
const sorted = named.map((m) => m.value).sort((a, b) => a - b);
const index = Math.ceil(sorted.length * 0.95) - 1;
return sorted[index];
}
generateReport(): string {
const metricNames = [...new Set(this.metrics.map((m) => m.name))];
const lines: string[] = ['### Runtime Performance Report\n'];
for (const name of metricNames) {
const avg = this.getAverage(name).toFixed(2);
const p95 = this.getP95(name).toFixed(2);
const count = this.getMetrics(name).length;
lines.push(`- **${name}**: avg=${avg}ms, p95=${p95}ms, n=${count}`);
}
return lines.join('\n');
}
}
const perfCollector = new PerformanceCollector();
async function fetchWithMetrics<T>(
url: string,
schema: z.ZodSchema<T>,
): Promise<T> {
return perfCollector.timeAsync(
'api.fetch',
async () => {
const response = await fetch(url);
const raw = await response.json();
const result = schema.safeParse(raw);
if (!result.success) {
throw new Error(`API validation failed: ${result.error.message}`);
}
return result.data;
},
{ url: new URL(url).pathname },
);
}
Common Pitfalls
Pitfall 1: skipLibCheck Causing Type Unsafety
// ❌ Global skipLibCheck disables all .d.ts checking
// tsconfig.json
{
"compilerOptions": {
"skipLibCheck": true // Disables all library type checking
}
}
// ✅ skipLibCheck only skips node_modules .d.ts, your own .d.ts still needs checking
// tsconfig.json
{
"compilerOptions": {
"skipLibCheck": true // Safe: only affects node_modules
}
}
// Your own types directory configured separately with strict checking
// types/tsconfig.json
{
"compilerOptions": {
"strict": true,
"skipLibCheck": false
}
}
Pitfall 2: Barrel Exports Breaking Tree-Shaking
// ❌ Re-exporting all modules in index.ts
// src/index.ts
export * from './users';
export * from './orders';
export * from './products';
export * from './analytics';
// ✅ Explicit on-demand exports
// src/index.ts
export { UserService } from './users/service';
export { OrderService } from './orders/service';
export type { User, CreateUserInput } from './users/types';
export type { Order, CreateOrderInput } from './orders/types';
// ✅ Configure sideEffects in package.json
// package.json
{
"sideEffects": false,
"module": "./dist/index.mjs",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./users": {
"import": "./dist/users.mjs"
}
}
}
Pitfall 3: Incomplete Project References Configuration
// ❌ Missing composite option causes reference to fail
// packages/shared/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist"
// Missing "composite": true
}
}
// ✅ Referenced projects must enable composite
// packages/shared/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
Pitfall 4: Excessive Runtime Type Guards
// ❌ Full type checking on every loop iteration
function processItems(items: unknown[]): Result[] {
return items.map((item) => {
const validated = ItemSchema.parse(item); // Full validation every time
return transform(validated);
});
}
// ✅ Validate once at entry, trust types internally
function processItems(items: unknown[]): Result[] {
const validated = z.array(ItemSchema).parse(items); // One-time validation
return validated.map(transform);
}
// ✅ Or use layered validation
function processItems(items: unknown[]): Result[] {
return items.map((item) => {
if (!isPlainObject(item)) throw new Error('Invalid item');
return transform(item as Item);
});
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
Pitfall 5: Ignoring Compiler Memory Limits
// ❌ Default 1.5GB memory insufficient for large projects
// Running tsc causes memory overflow
// FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
// ✅ Increase Node.js memory limit
// package.json
{
"scripts": {
"typecheck": "NODE_OPTIONS='--max-old-space-size=4096' tsc --build",
"typecheck:ci": "NODE_OPTIONS='--max-old-space-size=8192' tsc --build"
}
}
// ✅ Split projects to reduce per-compiler memory requirements
// Use Project References to split large projects into multiple sub-projects
// Each sub-project compiles independently, significantly reducing peak memory
Error Troubleshooting Table
| Error Symptom | Possible Cause | Solution |
|---|---|---|
tsc exceeds 30 seconds |
Single large project without Project References | Split into multiple sub-projects, enable incremental compilation |
error TS6307 |
Project References missing composite | Add "composite": true to referenced project |
| Abnormally large bundle | Barrel exports breaking tree-shaking | Switch to explicit named exports, configure sideEffects |
| Slow HMR refresh | tsc full re-check | Enable incremental and isolatedModules |
FATAL ERROR: Reached heap limit |
Compiler memory overflow | Increase --max-old-space-size or split project |
| Type inference timeout | Excessive recursive type depth | Limit recursion depth, use FlattenN<T, 3> |
Type instantiation is excessively deep |
Nested conditional types too deep | Simplify type logic, use intermediate type aliases |
| Poor runtime performance | Frequent type guards on hot paths | Layered validation, minimize hot path checks |
Type errors after skipLibCheck |
Custom .d.ts has issues | Configure custom types directory with separate strict checking |
| Unused code in bundle | tsconfig importsNotUsedAsValues |
Use tsup/esbuild instead of tsc for bundling |
Advanced Optimization
Compiler Performance Tuning
// tsconfig.perf.json - Maximum performance configuration
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./.tsbuildinfo",
"skipLibCheck": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"assumeChangesOnlyAffectDirectDependencies": true,
"disableSourceOfProjectReferenceRedirect": true,
"disableReferencedProjectLoad": true
}
}
// scripts/tsc-watch.ts - Smart incremental watcher
import { watch } from 'chokidar';
import { execSync } from 'child_process';
import { relative, dirname, extname } from 'path';
import { performance } from 'perf_hooks';
interface WatchConfig {
include: string[];
exclude: string[];
debounceMs: number;
projects: Record<string, string[]>;
}
const watchConfig: WatchConfig = {
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts', 'src/**/*.spec.ts'],
debounceMs: 300,
projects: {
'packages/shared': ['packages/shared/src/**/*.ts'],
'packages/core': ['packages/core/src/**/*.ts'],
'packages/app': ['packages/app/src/**/*.ts'],
},
};
let debounceTimer: NodeJS.Timeout | null = null;
function getAffectedProject(filePath: string): string | null {
for (const [project, patterns] of Object.entries(watchConfig.projects)) {
for (const pattern of patterns) {
const dir = pattern.replace('/**/*.ts', '');
if (filePath.startsWith(dir)) {
return project;
}
}
}
return null;
}
function typecheckAffected(filePath: string): void {
const project = getAffectedProject(filePath);
if (!project) return;
const start = performance.now();
try {
execSync(`npx tsc --build ${project}`, { encoding: 'utf-8', stdio: 'pipe' });
const duration = (performance.now() - start).toFixed(0);
console.log(`✓ ${project}: ${duration}ms`);
} catch (error: any) {
const duration = (performance.now() - start).toFixed(0);
console.error(`✗ ${project}: ${duration}ms`);
console.error(error.stdout || error.message);
}
}
const watcher = watch(watchConfig.include, {
ignored: watchConfig.exclude,
persistent: true,
ignoreInitial: true,
});
watcher.on('change', (filePath) => {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
typecheckAffected(filePath);
}, watchConfig.debounceMs);
});
watcher.on('add', (filePath) => {
typecheckAffected(filePath);
});
console.log('🔍 Watching for TypeScript changes...');
Bundle Optimization Strategies
// scripts/advanced-bundle-opt.ts - Advanced bundle optimization
import { build, Plugin } from 'esbuild';
const sizeLimitPlugin: Plugin = {
name: 'size-limit',
setup(build) {
build.onEnd((result) => {
for (const output of result.outputFiles || []) {
const sizeKB = output.contents.byteLength / 1024;
const path = output.path;
if (sizeKB > 50) {
console.warn(`⚠️ ${path}: ${sizeKB.toFixed(1)}KB exceeds 50KB chunk limit`);
}
}
});
},
};
const deadCodeEliminationPlugin: Plugin = {
name: 'dead-code-elimination',
setup(build) {
build.onEnd((result) => {
const content = result.outputFiles?.[0]?.text || '';
const deadCodePatterns = [
/if\s*\(\s*false\s*\)/g,
/if\s*\(\s*true\s*\)\s*\{/g,
/\/\/\s*@ts-ignore/g,
];
for (const pattern of deadCodePatterns) {
const matches = content.match(pattern);
if (matches && matches.length > 0) {
console.warn(`⚠️ Potential dead code: ${matches.length} occurrences of ${pattern}`);
}
}
});
},
};
async function buildOptimized(): Promise<void> {
await build({
entryPoints: ['src/index.ts'],
bundle: true,
minify: true,
treeShaking: true,
format: 'esm',
target: 'es2022',
outdir: 'dist',
metafile: true,
splitting: true,
plugins: [sizeLimitPlugin, deadCodeEliminationPlugin],
external: ['react', 'react-dom', 'zod'],
define: {
'process.env.NODE_ENV': '"production"',
},
logLevel: 'info',
});
}
buildOptimized();
Runtime Performance Profiling
// src/monitoring/profiler.ts - Runtime performance profiler
interface ProfileEntry {
operation: string;
startTime: number;
endTime: number;
duration: number;
memoryBefore: number;
memoryAfter: number;
memoryDelta: number;
}
class RuntimeProfiler {
private entries: ProfileEntry[] = [];
private readonly maxEntries = 500;
profile<T>(operation: string, fn: () => T): T {
const memBefore = process.memoryUsage().heapUsed;
const startTime = performance.now();
try {
return fn();
} finally {
const endTime = performance.now();
const memAfter = process.memoryUsage().heapUsed;
this.entries.push({
operation,
startTime,
endTime,
duration: endTime - startTime,
memoryBefore: memBefore,
memoryAfter: memAfter,
memoryDelta: memAfter - memBefore,
});
if (this.entries.length > this.maxEntries) {
this.entries = this.entries.slice(-this.maxEntries);
}
}
}
async profileAsync<T>(operation: string, fn: () => Promise<T>): Promise<T> {
const memBefore = process.memoryUsage().heapUsed;
const startTime = performance.now();
try {
return await fn();
} finally {
const endTime = performance.now();
const memAfter = process.memoryUsage().heapUsed;
this.entries.push({
operation,
startTime,
endTime,
duration: endTime - startTime,
memoryBefore: memBefore,
memoryAfter: memAfter,
memoryDelta: memAfter - memBefore,
});
if (this.entries.length > this.maxEntries) {
this.entries = this.entries.slice(-this.maxEntries);
}
}
}
getHotspots(thresholdMs: number = 10): ProfileEntry[] {
return this.entries
.filter((e) => e.duration > thresholdMs)
.sort((a, b) => b.duration - a.duration);
}
getMemoryHotspots(thresholdBytes: number = 1024 * 100): ProfileEntry[] {
return this.entries
.filter((e) => e.memoryDelta > thresholdBytes)
.sort((a, b) => b.memoryDelta - a.memoryDelta);
}
generateReport(): string {
const lines: string[] = [
'### Runtime Performance Profile\n',
'#### Time Hotspots (>10ms)',
];
for (const entry of this.getHotspots()) {
lines.push(
`- **${entry.operation}**: ${entry.duration.toFixed(2)}ms, ` +
`memory: ${(entry.memoryDelta / 1024).toFixed(1)}KB`
);
}
lines.push('\n#### Memory Hotspots (>100KB)');
for (const entry of this.getMemoryHotspots()) {
lines.push(
`- **${entry.operation}**: ${(entry.memoryDelta / 1024).toFixed(1)}KB, ` +
`time: ${entry.duration.toFixed(2)}ms`
);
}
return lines.join('\n');
}
}
const profiler = new RuntimeProfiler();
// Usage example
const users = profiler.profile('fetchUsers', () => {
return Array.from({ length: 1000 }, (_, i) => ({
id: `user-${i}`,
name: `User ${i}`,
email: `user${i}@example.com`,
}));
});
const result = profiler.profile('processUsers', () => {
return users.filter((u) => u.email.includes('example')).map((u) => u.name);
});
console.log(profiler.generateReport());
Solution Comparison
| Optimization Dimension | tsc Incremental | Project References | tsup/esbuild | Vite | Turbopack |
|---|---|---|---|---|---|
| Compilation Speed | ★★★ | ★★★★ | ★★★★★ | ★★★★★ | ★★★★★ |
| Configuration Complexity | Low | Medium | Low | Low | Low |
| Incremental Support | Yes | Yes | No (full but very fast) | Yes | Yes |
| Type Checking | Built-in | Built-in | External (tsc) | External (tsc) | External (tsc) |
| Tree-Shaking | No | No | Yes | Yes | Yes |
| Bundle Size | N/A | N/A | Excellent | Excellent | Excellent |
| Use Case | Library dev | Large projects | Library/Tools | Web apps | Next.js |
| Production Ready | ★★★★★ | ★★★★★ | ★★★★★ | ★★★★★ | ★★★★ |
| Runtime Optimization | Type Guard Optimization | Branded Types | const assertions | Generators | Layered Validation |
|---|---|---|---|---|---|
| Performance Gain | ★★★★ | ★★★ | ★★ | ★★★ | ★★★★ |
| Code Intrusion | Medium | Low | Low | Medium | Medium |
| Type Safety | ★★★ | ★★★★★ | ★★★★★ | ★★★ | ★★★★ |
| Learning Cost | Low | Low | Low | Medium | Low |
| Use Case | Hot paths | ID types | Constant config | Large datasets | API boundaries |
TypeScript performance optimization isn't "icing on the cake" — it's the "lifeline" of production projects. At compile-time, use Project References and incremental compilation for 10x speedup; at build-time, replace tsc with tsup/esbuild for tree-shaking; at runtime, avoid hot-path type guards; for memory, use Branded Types and const assertions to reduce bloat; in production, use performance budgets and type coverage to guard the baseline. Performance isn't optimized — it's designed. Configure Project References from day one, use explicit exports from the first module, and use const assertions from the first type.
Recommended Tools
- JSON Formatter — Format tsconfig and build output JSON, quickly troubleshoot configuration issues
- Code Formatter — Format TypeScript code, unify team code style
- cURL to Code — Convert API requests to type-safe TypeScript fetch code
Try these browser-local tools — no sign-up required →