I like TypeScript. I also ship plenty of code that never sees tsc. The
difference is not ideology, it is whether a compile step earns its keep for the
lifetime of the thing I am building.
Small internal tools die from ceremony. A banner generator used twice a quarter
does not need a tsconfig, a bundler profile, and a debate about
moduleResolution. It needs to open, work, and not frighten the next person who
touches it six months later.
When raw JS is the responsible choice
I stay in JavaScript when most of these are true:
- Single file or a tiny folder: a Node script, a Cloudflare Worker sketch, a browser bookmarklet-adjacent page.
- One author in the near term: or a pair that already shares conventions.
- Runtime is the product: you open DevTools or run
node script.jsand see the truth. No "it typechecks but the emit is wrong" layer. - Dependencies stay shallow: preferably zero beyond the platform.
A pattern I still use for throwaway admin pages:
html<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <title>CSV → JSON</title> <style> body { font: 14px/1.4 system-ui, sans-serif; max-width: 40rem; margin: 2rem auto; padding: 0 1rem; } textarea { width: 100%; min-height: 12rem; } </style> </head> <body> <h1>CSV → JSON</h1> <textarea id="input" placeholder="paste csv" ></textarea> <button id="run" type="button" > Convert </button> <pre id="out"></pre> <script type="module"> const input = document.querySelector('#input'); const out = document.querySelector('#out'); function parseCsv(text) { const [headerLine, ...rows] = text.trim().split(/\r?\n/); const headers = headerLine.split(','); return rows.map((line) => { const cells = line.split(','); return Object.fromEntries(headers.map((h, i) => [h, cells[i] ?? ''])); }); } document.querySelector('#run').addEventListener('click', () => { out.textContent = JSON.stringify(parseCsv(input.value), null, 2); }); </script> </body> </html>
Ugly? A little. Done in one sitting? Yes. Deployable by dropping a file on any static host? Also yes. That is the point.
For Node-side chores I reach for plain ESM:
js#!/usr/bin/env node import { readFileSync, writeFileSync } from 'node:fs'; const [, , inPath, outPath] = process.argv; if (!inPath || !outPath) { console.error('usage: rename-keys.mjs <in.json> <out.json>'); process.exit(1); } const data = JSON.parse(readFileSync(inPath, 'utf8')); const next = data.map(({ user_id, ...rest }) => ({ userId: user_id, ...rest, })); writeFileSync(outPath, JSON.stringify(next, null, 2) + '\n');
No build. Runnable on a coworker laptop with a modern Node. The feedback loop is the shell.
What you quietly give up
Skipping TypeScript is not free. You give up:
- Rename confidence across files.
- Editor breadcrumbs for function contracts without reading the implementation.
- A forced design conversation about nullability and unions.
Raw JS asks you to compensate with smaller modules, clearer names, and tests where the cost of being wrong is high. If you skip the compiler and skip tests on money-moving logic, that is not pragmatism, that is optimism.
I treat browser type="module" scripts and small Node CLIs as the sweet spot.
The moment the tool grows a shared library used by two apps, the cost curve
flips.
The failure modes that pushed me out
These are the triggers that make me add a compiler (or at least JSDoc checking):
- Third consumer appears. Copy-paste drifts. The "tiny script" is now an undocumented API.
- Shape of data gets sharp. Nested billing payloads, partial records, feature flags, stringly typed objects start producing production bugs that a type would have blocked at edit time.
- Refactors take longer than features. If I am scared to rename
optstooptionsacross eight files, the project has outgrown vibes-based typing. - Onboarding cost rises. A new teammate should not have to reverse-engineer every function's return value from runtime logs.
None of those mean "always TypeScript on day one." They mean I watch for the curve bending and move before the rewrites get romanticized in retrospectives.
Productivity is latency to a correct change
People sometimes frame this as "JS is faster to write." The version I believe is narrower: for a bounded problem, fewer moving parts produce correct results sooner. A build pipeline, path aliases, and declaration emit are moving parts. They pay off when the program's surface area justifies them.
I will keep writing raw JS for one-off converters, migration helpers I run once, and prototypes that need to die cheaply. I will not pretend that choice scales to a multi-package product surface.
Next in this TypeScript arc: meeting in the middle with JSDoc annotations - types without forcing a full compile-to-JS workflow on every save.