Home › Programming languages › Dynamic & glue
guide low

TypeScript and JavaScript

Problem Is TypeScript or JavaScript the right choice for browser UIs and for server code, and where does its single-threaded model become a problem? This guide helps decide where it fits.

The only first-class browser language, also a fast-enough server runtime with a single-threaded event loop; TypeScript adds static types that vanish at runtime.

typescriptjavascriptnodejsv8event-loopfrontendbundeno
Overview

JavaScript engines JIT-compile dynamic code to fast machine code, typically order of 2-5x of C on hot loops. Node.js, Deno and Bun run it on servers with one event-loop thread per process: non-blocking I/O scales to many connections, but any CPU-heavy callback stalls everyone. TypeScript's types are checked at build time and erased, so runtime data still needs validation.

Key facts

Use when

  • Browser UIs, full-stack apps sharing types, BFFs, WebSocket apps, edge functions, tooling.
  • Real uses by field: web (the language of browser pages), edge serverless (Cloudflare Workers run JavaScript in V8 isolates), mobile apps (React Native renders React primitives as native UI on Android and iOS), desktop apps (Electron embeds Chromium and Node.js).

Avoid when

  • CPU-bound services or work needing predictable low tail latency: offload to Rust/Go or WASM.

Deep dive

Decision table

JobPickWhy
Web frontendTypeScript + React (or Vue, Svelte, Solid)Ecosystem and hiring
Full-stack web appNext.js, Remix/React Router, SvelteKit, NuxtSSR, routing, data loading
API serverNode + Fastify or Hono; NestJS for large teamsMature, fast enough
Edge / serverless functionsHono or plain handlers on Cloudflare Workers / Vercel / Deno DeployV8 isolates, ms cold starts
Fast tooling and scriptsBun or DenoBuilt-in TS, test runner, bundler
Runtime validationzod / valibot (types inferred from schemas)One source of truth for type and check
Desktop appElectron (mature) or Tauri (Rust shell, smaller)Web skills on desktop
MobileReact Native / ExpoShared logic with web
CPU hot pathRust → WASM, or a native addon (napi-rs)Escape the JIT and GC

Memory and performance model

  • V8: interpreter (Ignition) → baseline and optimizing JITs (Sparkplug, Maglev, TurboFan) guided by type feedback. Hidden classes and inline caches make objects with stable shapes fast; changing shapes or mixing types in one array deoptimizes.
  • Generational GC (Orinoco): young-generation scavenges are cheap; old-generation marking is mostly concurrent. Heap size limit set by --max-old-space-size (MiB).
  • Typed arrays (Float64Array, Uint8Array) give contiguous, unboxed memory.
  • Startup: Node order of 30-50 ms; V8 isolates on edge platforms start in order of ms.

Concurrency model

  • Event loop: timers → I/O callbacks (libuv over epoll/kqueue/IOCP) → microtasks (promise continuations) after each task. await yields to the loop.
  • File system and DNS calls use libuv's thread pool (default 4 threads, UV_THREADPOOL_SIZE).
  • worker_threads for CPU work, communicating by postMessage (structured clone) or SharedArrayBuffer + Atomics.
  • Scale across cores with multiple processes (cluster, or one container per core behind a load balancer).

Code

TypeScript (Node 22+, Bun or Deno; npm i zod): validate at the boundary, time out every request, bound concurrency.

import { z } from "zod";

const User = z.object({
  id: z.number().int().positive(),
  email: z.string().email(),
  tags: z.array(z.string()).max(20).default([]),
});
type User = z.infer<typeof User>; // static type derived from the runtime schema

async function fetchJson(url: string, ms = 5_000): Promise<unknown> {
  const res = await fetch(url, { signal: AbortSignal.timeout(ms) }); // never hang forever
  if (!res.ok) throw new Error(`${url}: HTTP ${res.status}`);
  return res.json(); // `unknown`, not `any`: forces validation
}

// At most `limit` promises in flight; results keep input order.
export async function mapLimit<T, R>(items: T[], limit: number, fn: (t: T) => Promise<R>): Promise<R[]> {
  const out = new Array<R>(items.length);
  let next = 0; // safe without locks: single-threaded, no await between read and increment
  const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
    while (next < items.length) {
      const i = next++;
      out[i] = await fn(items[i]!);
    }
  });
  await Promise.all(workers);
  return out;
}

export async function loadUsers(ids: number[]): Promise<User[]> {
  const settled = await mapLimit(ids, 8, (id) =>
    fetchJson(`https://api.example.com/users/${id}`)
      .then((j) => ({ ok: true as const, user: User.parse(j) }))
      .catch((err: unknown) => ({ ok: false as const, err })),
  );
  const users: User[] = [];
  for (const r of settled) {
    if (r.ok) users.push(r.user);
    else console.error("skipped:", r.err);
  }
  return users;
}

process.on("unhandledRejection", (e) => { console.error(e); process.exit(1); }); // fail loudly

tsconfig.json for a Node service:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true,
    "outDir": "dist"
  }
}

Efficiency

  • Cost: hot JIT-compiled loops run order of 2-5x slower than C; every number is a float64. A Cloudflare Workers isolate is created inside an already running runtime, which removes the virtual-machine cold start (Cloudflare docs). libuv's thread pool has 4 threads by default (libuv docs). V8's young-generation GC copies only survivors, so its cost scales with surviving objects, not allocations (V8 blog). Bound: one thread's CPU for compute, the event loop for latency.
  • Levers:
    • objects with one stable shape (same hidden class) — keeps inline caches monomorphic (V8 docs) — hot property accesses
    • typed arrays instead of object arrays — contiguous unboxed memory (order of) — numeric buffers
    • worker_threads — parallel CPU work; they do not help I/O-bound work (Node.js docs) — CPU-bound handlers
    • UV_THREADPOOL_SIZE — more concurrent fs/DNS/crypto jobs than the default 4 (libuv docs) — heavy file or hash workloads
    • Rust compiled to WASM or a native addon — escapes JIT and GC (order of) — CPU hot paths
  • Naive → standard → state of the art:
    • CPU work inline in the request handler · blocks the event loop for every client
    • worker_threads or one process per core · parallel, structured-clone message cost
    • hot kernel in WASM or a native addon, shared via SharedArrayBuffer · near-native, no copy

Build and deploy

  • Type-check with tsc --noEmit in CI; transpile with esbuild/swc/tsc, or run .ts directly (Bun, Deno, recent Node with type stripping for erasable syntax).
  • Lockfile + npm ci (or pnpm); disable install scripts for dependencies where possible (see supply chain).
  • Containers: node:22-slim or distroless Node, NODE_ENV=production, non-root, heap limit below the container limit.
  • Frontend: Vite for bundling and dev server; ship modern ES modules, code-split routes.

Pitfalls

  • Blocking the event loop: JSON.parse of huge payloads, sync crypto/fs, regex backtracking, big loops in a request handler.
  • Floating promises (no await, no .catch): errors vanish or crash later; enable @typescript-eslint/no-floating-promises.
  • any and as assertions spreading through the codebase; types lie, runtime explodes.
  • Assuming === on objects compares contents, or sort() sorts numbers (it sorts strings by default).
  • Money in floats; IDs above 2^53 parsed from JSON losing precision (use strings).
  • Dependency sprawl: thousands of transitive packages; audit and prune.
  • Memory leaks from closures captured by long-lived listeners or caches without bounds.
  • localStorage for auth tokens (XSS-readable); prefer HttpOnly cookies.

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
Node.jsC++ / JSnodejs/nodebrew install node————V8 + libuv; most used server runtime
TypeScriptTypeScriptmicrosoft/TypeScriptnpm install typescript --save-dev————tsc, type checker
DenoRustdenoland/denocurl -fsSL https://deno.land/install.sh | sh————runs TypeScript directly, permission model
BunZigoven-sh/buncurl -fsSL https://bun.com/install | bash————runtime, package manager, bundler, test runner
ViteTypeScriptvitejs/vitenpm create vite@latest————frontend dev server and bundler

Sources

  • Ecma International, ECMA-262: ECMAScript Language Specification. spec
  • Microsoft, TypeScript Handbook. docs
  • OpenJS Foundation, Node.js documentation: The Node.js Event Loop, Timers, and process.nextTick() docs · worker_threads docs
  • V8 project, v8.dev blog (Ignition, TurboFan, Sparkplug, Maglev, Orinoco). blog · Trash talk: the Orinoco garbage collector blog · Maps (Hidden Classes) docs
  • libuv, Thread pool work scheduling. docs
  • Cloudflare, How Workers works. docs
  • Meta, React Native. site · OpenJS Foundation, Electron. site

Alternatives and related

Choosing a languageWhich programming language should you use for a given job, and when is a rewrite or a mix of languages worthwhile? This guide compares languages by speed, memory, hiring and build cost.PythonIs Python the right language for ML, data work, scripting or a backend, and how do you avoid its slowness? This guide covers where it pays off and where it does not.GoIs Go the right language for a network service, proxy or infrastructure tool? This guide covers what it is good at, and where tail latency or heavy numerics call for something else.RustIs Rust the right language when you need native speed without a garbage collector and without memory-safety bugs? This guide weighs that against its learning curve and compile times.epoll event loopA server must keep tens of thousands of network connections open and react to whichever have data, without a thread per connection and without checking every connection on each wakeup.Software supply chain securityTeams shipping software with third-party dependencies and a deploying CI system must decide how to control what code enters the build and prove what comes out, against malicious packages and compromised build tooling.CDN and edgeUsers far from your servers see slow responses, and traffic spikes or attacks hit the origin directly. This guide answers whether and how to put a CDN or edge compute in front of a service.Serverless functionsTeams with spiky or low-traffic workloads (webhooks, cron jobs, event handlers) want to avoid running idle servers. This guide answers when functions-on-demand fit and when they cost more or fall short.