· 7 min read
Hacking Performance: How TypeScript Tricks Can Speed Up Your Application
Practical TypeScript techniques - from tsconfig choices to code patterns - that reduce build time, shrink bundles, and help JavaScript engines optimize hot code paths without sacrificing clarity.
Introduction
TypeScript is primarily a developer experience tool: types help you catch bugs earlier and improve maintainability. But TypeScript also indirectly - and sometimes directly - affects runtime performance and bundle size. Small changes in how you write types or configure the compiler can shrink emitted JavaScript, reduce allocations, help the JIT optimize, and speed up builds.
This post collects practical, battle-tested TypeScript “tricks” and guidelines you can apply today to improve build and runtime performance while keeping code clear.
Why TypeScript choices matter for performance
- Some TypeScript features only affect compile-time types and are erased at runtime (no cost). Others change emitted JavaScript, which affects bundle size and runtime behavior.
- Compiler options influence transpilation strategy (helpers vs inline, target JS language level) and can dramatically change output size and performance.
- Type-driven code patterns affect how you structure objects and functions, and those shapes and allocations are critical for VM optimization (V8 TurboFan / hidden classes, inline caches).
Key areas we’ll cover
- Compiler / build-time configuration that reduces work and output size
- TypeScript-specific code patterns that improve emitted JS or JIT friendliness
- Micro-optimizations: allocation reduction, numeric arrays, loops
- Measuring impact and where to focus your effort
Build-time and bundling hacks (tsconfig and tooling)
- Target modern JS to avoid heavy downlevel helpers
If you can run on modern runtimes (Node 14+/browsers with ES2017+), set a higher target in tsconfig. Transpiling to older targets injects helpers (generators, downleveled async/await, iterator helpers), increasing code size and sometimes adding runtime overhead.
Example tsconfig snippet:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Node",
"lib": ["ES2020"]
}
}
Why: keeping native constructs like async/await and native iteration lets the JS engine use its optimized implementations rather than emulated helpers. See V8 notes on optimization: https://v8.dev/docs/optimizing-javascript
- Use ES modules to enable tree-shaking
Emit ESM (module: “ESNext” or similar) so bundlers (Webpack/Rollup/ESBuild) can tree-shake unused exports, reducing bundle size.
Docs: tree-shaking with Webpack - https://webpack.js.org/guides/tree-shaking/
- Import helpers from tslib instead of inlining
Set importHelpers: true and add tslib to dependencies. This prevents TypeScript from duplicating helper functions (like **extends, **assign) across files and reduces bundle size.
{
"compilerOptions": {
"importHelpers": true,
"downlevelIteration": false
}
}
Docs: https://www.typescriptlang.org/tsconfig#importHelpers
- Use incremental / project references for large monorepos
Turn on incremental builds and consider project references to break a huge project into smaller TypeScript projects. This dramatically speeds up compile times during active development.
- incremental: https://www.typescriptlang.org/tsconfig#incremental
- project references: https://www.typescriptlang.org/docs/handbook/project-references.html
- Prefer type-only imports to remove runtime imports
If you only need a type and not the runtime value, use import type. That prevents the compiler from emitting a runtime require/import for that symbol - helpful for avoiding accidental side-effect imports and improving bundling.
// type-only import - no runtime code emitted
import type { User } from './models';
// runtime import for the value
import { format } from './format';
See TypeScript release notes: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
- Avoid transpiling helpers with Babel without tslib
If you use Babel to transpile TypeScript, ensure you also use tslib or configure Babel to avoid emitting duplicated helpers. Otherwise each compiled file may carry its own helper functions, inflating bundle size.
Runtime-level TypeScript tricks that affect hot code
- Use const enums when appropriate (with caution)
const enums are inlined at compile time - they disappear from runtime as an object and get replaced with numeric or string literals. That reduces runtime lookups and object allocations.
// TypeScript
const enum Color {
Red,
Blue,
}
const c = Color.Red; // compiled as 0
Caveats: const enums require full TypeScript compilation (they can break when using Babel-only pipelines) and are not visible at runtime for reflection. Docs: https://www.typescriptlang.org/docs/handbook/enums.html#const-enums
- Declare object shapes and class fields up-front to keep hidden classes stable
Modern JS engines optimize objects by assigning them hidden classes (shapes). Adding or removing properties dynamically changes the object’s hidden class and deoptimizes code. TypeScript encourages explicit properties: declare them on classes or use constructors to define all fields so every instance has the same shape.
Bad (shape changes):
class Bad {
constructor() {}
}
function make() {
const b = new Bad();
if (Math.random() > 0.5) b.a = 1;
return b;
}
Good (stable shape):
class Good {
a: number | undefined;
b: string | undefined;
constructor() {
this.a = undefined;
this.b = undefined;
}
}
TypeScript helps by letting you declare the properties (and their types), nudging you toward consistent shapes. See V8 hidden classes: https://v8.dev/docs/hidden-classes
- Prefer Maps for large dynamic key sets
If you store many dynamic keys or non-string keys, prefer Map over plain objects. Maps have predictable insertion/lookup performance and fewer pitfalls with prototype keys.
MDN Map docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
- Use typed arrays for numeric-hot loops
When working with numeric, contiguous data (graphics, DSP, physics), use TypedArray (Float32Array, Int32Array) to reduce memory overhead and get faster numeric loops (and potential WebAssembly interop). TypeScript types for typed arrays are native, so you get type safety for free.
const n = 1_000_000;
const a = new Float32Array(n);
for (let i = 0; i < n; i++) {
a[i] = i * 0.5;
}
MDN typed arrays: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays
- Avoid creating functions/closures in hot loops
This is generic JS advice but TypeScript’s type system makes it easy to centralize function signatures and reuse implementations.
Bad:
for (let i = 0; i < n; i++) {
array.forEach(x => {
/* closure allocated each iteration */
});
}
Better: hoist the callback
function process(x: number) {
/* ... */
}
for (let i = 0; i < n; i++) {
array.forEach(process);
}
- Use union/discriminated unions to avoid runtime type checks
When you model variants using discriminated unions, your runtime control flow becomes simple switch/case statements and you avoid ad-hoc runtime type-checking. This keeps code fast and readable.
type Shape =
| { kind: 'circle'; r: number }
| { kind: 'rect'; w: number; h: number };
function area(s: Shape) {
switch (s.kind) {
case 'circle':
return Math.PI * s.r * s.r;
case 'rect':
return s.w * s.h;
}
}
TypeScript compiles this to a straight switch on a string field - no reflective checks, smaller code than many ad-hoc branches.
- Prefer short-circuiting / early returns vs deep nested checks
Concise control flow usually results in fewer branches and smaller emitted code. TypeScript’s non-null/optional chaining (?.) can reduce boilerplate and prevent accidental allocations for intermediate temporaries.
Micro-optimizations that matter in hot paths
- Reduce allocations: reuse arrays/objects when safe.
- Pre-size arrays when length is known: const arr = new Array(n).
- Use for (let i = 0; i < n; i++) indexed loops for array numeric loops - they can be faster than for..of in tight numeric work (depends on runtime).
- Avoid Object.assign/spread in hot loops; prefer manual assignment when performance-critical.
Example: avoid repeated object spreading
Bad:
for (let i = 0; i < n; i++) {
obj = { ...obj, x: i };
}
Better (mutate known object or create new with a single allocation outside hot loop):
const o = {} as { x?: number };
for (let i = 0; i < n; i++) {
o.x = i;
}
Measuring, profiling, and validating
- Measure before you optimize. Use realistic scenarios and profiler tools: Chrome DevTools/Performance, Node.js —prof, or Flamegraphs.
- Pay attention to allocations (memory profiler) and function hotness (CPU profiler). The places with many allocations or deoptimized frames are the best targets.
- Verify bundle size changes after tsconfig changes with source maps and bundle analyzers (Webpack Bundle Analyzer, source-map-explorer).
Helpful links for profiling:
- V8 optimizing JavaScript: https://v8.dev/docs/optimizing-javascript
- Node.js performance guide: https://nodejs.org/en/docs/guides/simple-profiling/
Practical checklist (apply incrementally)
- Set target/module to modern JS where feasible (reduce transpilation helpers)
- Enable importHelpers + install tslib
- Emit ESM for tree-shaking
- Use type-only imports (import type) when you only need types
- Use incremental + project references for large projects
- Declare class fields/props up front to stabilize object shapes
- Replace heavy object spreads in hot loops; reuse objects/arrays
- Use Map for dynamic key collections and TypedArrays for numeric arrays
- Prefer const enums carefully, knowing the caveats
- Profile and validate any change with real workloads
When to ignore micro-optimizations
- Don’t prematurely optimize. Most apps won’t benefit from micro-optimizations at the cost of clarity.
- Optimize where profiling shows hotspots or where bundle size is a real problem (e.g., slow network or large mobile footprint).
Conclusion
TypeScript is more than types: choosing the right compiler options and idiomatic type-driven patterns can reduce bundle size, speed builds, and produce JS that the engine can optimize more effectively. Focus on the knobs that give big wins (modern targets, importHelpers, ESM output, stable object shapes), then profile and refine hot spots with targeted micro-optimizations.
References
- TypeScript Handbook / tsconfig: https://www.typescriptlang.org/docs/
- importHelpers (tsconfig): https://www.typescriptlang.org/tsconfig#importHelpers
- Type-only imports (TS 3.8 release notes): https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
- Const enums: https://www.typescriptlang.org/docs/handbook/enums.html#const-enums
- V8 Optimizing JavaScript & hidden classes: https://v8.dev/docs/optimizing-javascript and https://v8.dev/docs/hidden-classes
- Tree-shaking guide (Webpack): https://webpack.js.org/guides/tree-shaking/
- MDN: Map - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
- MDN: Typed arrays - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays