background-shape
Why TypeScript Is Winning Over JavaScript in 2023
February 2, 2023 · 8 min read · by Muhammad Amal programming

TL;DR — TypeScript adoption hit critical mass in 2022 and is the default for new backends in 2023 / 4.9’s satisfies operator closed the last expressiveness gap that pushed people back to JS / The build cost is real but smaller than ever with tsx, swc, and esbuild.

I’ve spent the past year deliberately starting every new Node service in TypeScript by default — even quick internal tools that previously would’ve been single-file JS. It’s not religion. It’s that the friction of TS in early 2023 is finally lower than the friction of debugging shape mismatches in JS at 11pm.

The State of JS 2022 survey landed in late January and the numbers are blunt: 78% of respondents used TypeScript in the last year, up from 30% in 2017. New projects on GitHub for backend Node services skew heavily TS-first now. The remaining JS holdouts cluster in three places — quick scripts, legacy codebases too big to migrate, and teams where one senior dev refused. We’ll get into all three.

This piece is the “why now” framing for the rest of February’s posts on TS migrations, Next.js 13, and where the language is heading with 5.0 around the corner.

What changed in 2022 that made 2023 feel different

TypeScript existed for a decade. So why does adoption suddenly feel finished, not in-progress?

A few things compounded:

The tooling stopped being a tax. swc and esbuild made transpilation roughly free. tsx (released mid-2022) lets you run a .ts file with no build step at all. ts-node was always slow enough to be an excuse; tsx removed the excuse. For dev-loop ergonomics, TS now matches JS — you save a file, you run it, it executes.

Editors caught up. VS Code with the built-in TS server gives you accurate types, fast renames, and the small dopamine hit of red squiggles before your code runs. JetBrains and Neovim users get the same via tsserver. There’s no longer a developer experience gap between “JS with good editor support” and “TS with good editor support” because the JS path mostly is the TS path under the hood.

The ecosystem stopped fighting it. Five years ago you’d find a perfect npm library, install it, and discover the types were missing or wrong, or only on @types/*. Today most actively-maintained libraries ship their own .d.ts. Prisma, Drizzle, Hono, tRPC, Zod — all type-first. Even Express finally accepted that the types are part of the contract.

4.9’s satisfies killed a long-standing complaint. I’ll talk about satisfies more in a later post this month, but it solved the “I want type-checking on this literal without losing its narrow type” problem that had pushed people to assertions and as const gymnastics.

// before 4.9: pick one — checking OR narrowing
const config = {
  port: 3000,
  host: "0.0.0.0",
  mode: "production",
} as const; // narrow, but no check that mode is a valid Mode

type Config = { port: number; host: string; mode: "dev" | "production" };

// after 4.9
const config = {
  port: 3000,
  host: "0.0.0.0",
  mode: "production",
} satisfies Config; // checked AND narrow — mode keeps its literal type

That’s a small example, but satisfies shows up everywhere you author configuration, routing tables, or const enums. It removed one of the last “TS makes me write worse code than JS” arguments.

The team-level reasons it wins

Personal preference aside, the team-level case for TypeScript is what makes companies standardize on it.

Refactoring confidence. Renaming a method across a 100k-line JS codebase is “grep, hope, run tests, find out in prod.” In TS it’s “F2 in your editor, done.” That’s not a marginal improvement, it’s a different activity. The bigger the codebase, the bigger the gap.

Onboarding new engineers. A new hire opening a TS file gets free documentation: hover any symbol, see what it accepts and returns. In JS they get filenames and JSDoc that may or may not still match the implementation. We measured time-to-first-meaningful-PR after a new hire onboarded to one of our services after a TS migration — it dropped from 9 days to 4. Not a controlled experiment, but consistent across three hires.

Contract enforcement at boundaries. With Zod parsing incoming requests and TS types flowing through your business logic, the place where bad data enters your system is the only place you check for it. Everything downstream gets to assume the data is well-formed. That’s the closest backend Node has ever come to feeling like Go or Rust around input validation.

Less defensive code. JS codebases accumulate if (!x) return; checks for things the author wasn’t sure could be undefined. TS tells you whether it can be. You delete a lot of defensive cruft after a serious migration.

I’ll have more concrete numbers in the migration post next week — including how to do it without freezing feature work.

Where JavaScript holdouts still have a point

I want to be fair to the people not jumping. They’re not all wrong.

Tiny scripts and one-offs. A 30-line CLI you’ll run twice doesn’t benefit from types. tsx makes the build cost negligible, but the cognitive cost of declaring types for throwaway code is real. Plain Node with JS or a single-file Python script is still the right tool for “I need to dedupe this CSV.” TS shines when code outlives the moment of writing it.

Legacy codebases past the migration cliff. Once a JS codebase passes ~50k lines without types, the migration cost gets steep. You can do it incrementally — allowJs: true, JSDoc-typed exports, file-by-file conversion — but at some point management has to fund the work and stomach the slowdown. Teams that have decided the ROI doesn’t justify it are making a reasonable call. The defect-rate paper from Microsoft (15% reduction post-TS-migration, airccj.org link — still cited five years later) is real, but it’s not infinite.

The type-acrobatics tax on library authors. If you’re writing a library API meant to be ergonomic for both JS and TS consumers, the type signatures can get genuinely cursed. Conditional types, recursive types, branded types — library authors live in a different version of TS than application developers. For application code in 2023, this argument doesn’t apply. For library authors, it’s still real.

Build pipeline complexity. Even with tsx, production builds still need a real compiler step somewhere. CI matrices grow. tsconfig files mutate. Project references and monorepos turn into yak-shaving sessions. It’s better than 2018, but it’s not zero.

The honest version of “should we use TypeScript” is: yes, unless you’re writing throwaway code, or you have a specific legacy situation that makes the migration unattractive.

What runtime support actually looks like in early 2023

One thing worth being precise about: TS doesn’t run anywhere natively yet. Every TS file gets transpiled to JS before execution. The differences between options:

# 1. tsc — official compiler, slowest, most accurate
npx tsc --noEmit          # type-check only
npx tsc --watch           # type-check loop

# 2. tsx — wraps esbuild, no type-check at runtime, fastest dev loop
npx tsx watch src/index.ts

# 3. swc — Rust-based, fast, used by Next.js and Deno
npx swc src -d dist

# 4. esbuild — Go-based, fast, common build target
npx esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js

The 2023 production stack is usually: tsc --noEmit in CI for type-checking, then swc or esbuild for actual transpilation. ts-node is mostly retired except in codebases that haven’t updated their tooling. There’s been chatter about Node and Deno adding native TS support, but Node has been clear it’s not on the near-term roadmap. Deno runs TS directly. Bun (still 0.x in Feb 2023) runs TS directly. Eventually this will be solved at the runtime layer; for now, transpilation is the contract.

Common Pitfalls

Even teams committed to TS still bite themselves on a few things:

  • any creep. Every any is a TODO you’ll never come back to. Treat noImplicitAny: true as non-negotiable from day one. unknown is almost always what you actually wanted.
  • Misusing as casts as type assertions. value as User doesn’t validate anything at runtime — it’s a promise to the compiler. Use Zod, io-ts, or hand-written guards at boundaries; reserve as for cases where you genuinely know more than the compiler.
  • strict: false projects. A TS project without strict: true is doing 30% of the work for 80% of the visual noise. Strict mode is where the actual safety lives. Anything else is type-flavored JavaScript.
  • Over-typing internal code. You don’t need a named type for every parameter. Inline shapes are fine for one-off function arguments. Save names for things that recur or cross module boundaries.
  • Ignoring tsconfig.json for too long. The defaults from tsc --init are loose for backwards compatibility. Read the docs and set strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes, target: ES2022, module: NodeNext for modern Node services.

Wrapping Up

The 2023 question isn’t “should we use TypeScript” — it’s “how do we get the codebase we already have into TypeScript without grinding feature work to a halt.” The rest of this month is about that journey, plus the Next.js 13 architectural shift that’s pulling even more JS shops across the line.

If you’re still on the fence, the experiment that convinces people is small: pick one new file in your service, give it a .ts extension, set allowJs: true in your tsconfig, and write it strict. That’s the whole pitch.