background-shape
TypeScript 5.0 Beta, What's Actually Changing
February 27, 2023 · 6 min read · by Muhammad Amal programming

TL;DR — Stage 3 decorators finally land, replacing the experimental flag / const type parameters tighten inference for generic helpers / Bundler module resolution simplifies tooling / Compiler is meaningfully faster from the ESM rewrite.

The TypeScript 5.0 beta dropped at the end of January 2023, with the stable release expected by mid-March. I’ve been running it on a side project for two weeks. The headline feature is decorators, but the small changes are what made me upgrade dev branches.

This post is the working summary of what’s worth knowing now — for backend devs especially. It closes the February TypeScript series with a forward look at where the language is heading.

Stage 3 decorators

The biggest change: decorators are no longer behind experimentalDecorators. The committee finalized the proposal at TC39 stage 3, and TypeScript 5.0 implements that spec.

The old syntax-via-experimentalDecorators is still supported behind the same flag for compatibility, but the new spec is incompatible with the old one. Existing decorator-heavy codebases (NestJS, TypeORM, TypeStack) won’t be moving overnight.

The new shape:

function logged(originalMethod: Function, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name);
  return function (this: unknown, ...args: unknown[]) {
    console.log(`calling ${methodName} with`, args);
    const result = originalMethod.apply(this, args);
    console.log(`${methodName} returned`, result);
    return result;
  };
}

class Calculator {
  @logged
  add(a: number, b: number) {
    return a + b;
  }
}

Three things to note:

  1. The decorator function signature is fixed by the spec. It takes (originalMethod, context) and returns a replacement.
  2. The context object has fields like kind ("method", "field", "class", etc.), name, static, private, and addInitializer for setup work.
  3. Parameter decorators are not in this proposal. That’s a meaningful gap if you use frameworks (NestJS, etc.) that rely on them. Those frameworks will keep using experimentalDecorators for now.

Backend devs writing your own decorators for, say, route registration or method logging can migrate to the new syntax. Backend devs using NestJS won’t migrate this year.

const type parameters

This one is subtle but useful. Generic helpers can now use const on their type parameters to ask for narrow inference.

Before 5.0:

function pick<T>(arr: T[]) {
  return arr[0];
}

const r = pick(["a", "b", "c"]);
// r: string (widened)

In 5.0 with const:

function pick<const T>(arr: readonly T[]) {
  return arr[0];
}

const r = pick(["a", "b", "c"]);
// r: "a" | "b" | "c" (narrow)

It’s the same idea as the satisfies operator from 4.9 — preserve the literal types — applied to generics. Useful when writing helper functions that work over const arrays, enum-like tuples, or routing tables.

I’ve started using const parameters in:

  • Validation helpers that take a tuple of allowed values
  • Builder helpers (route builders, form schema builders)
  • DSL-like utilities that need to remember the user’s literals

For application code outside libraries, you’ll use this maybe twice a quarter.

Bundler module resolution

moduleResolution: "bundler" is a new mode. Previously the options were node (old CommonJS rules), node10 (alias for node), node16, nodenext, and classic. The bundler mode acknowledges that lots of code is meant for esbuild, Vite, Webpack, or another bundler that doesn’t follow Node’s strict ESM rules — particularly around explicit file extensions.

// tsconfig.json
{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "bundler"
  }
}

What this fixes: you can import from "./utils" without writing "./utils.js". You can import package subpaths that don’t have explicit exports entries. You can import .ts files. The compiler stops asking you to satisfy Node’s exact-extension rules, because in a bundler-based project those rules don’t apply.

For backend Node projects, stick with nodenext. For frontend or universal-build projects, bundler is correct.

Performance: ESM migration paid off

The TypeScript team migrated the compiler’s own source from namespaces to ECMAScript modules. The compiler is roughly 10-20% faster on most workloads as a result of the cleaner module graph and tighter dead code elimination.

You won’t feel this on tiny projects. On a 50k+ line codebase with project references, the difference is noticeable on cold compiles — going from 14 seconds to 11 in my measurements on the project I tested.

Build memory is also slightly down. Not a feature you celebrate, but a quality-of-life improvement.

Smaller features worth knowing

  • All enums are union enums. Numeric enums used to have widening behavior that surprised people. Now they consistently behave as unions of their literal members.
  • --moduleResolution bundler + --allowImportingTsExtensions. Pair these to write import { x } from "./foo.ts" directly in bundler-targeted code.
  • --verbatimModuleSyntax. Replaces the older importsNotUsedAsValues and preserveValueImports. Forces explicit import type for type-only imports. Improves bundler tree-shaking.
  • resolution-mode assertions. You can now mark individual imports as CommonJS or ESM, useful in dual-published packages.

What it doesn’t fix

A few longstanding pain points remain:

  • Path mapping at runtime. TypeScript’s paths setting is compile-time only. Node still doesn’t resolve @/lib/foo. You still need a tsconfig-paths or bundler equivalent for runtime support.
  • Type-only side effects. Frameworks (NestJS, Mikro-ORM) that use parameter decorators or type metadata via reflect-metadata still need emitDecoratorMetadata: true and experimentalDecorators: true. The new decorator spec doesn’t carry type metadata.
  • Faster type-checking for huge unions. Big discriminated unions still slow the compiler. The work for this is ongoing but didn’t land in 5.0.

Should you upgrade

For application code:

  • Greenfield project starting now: 5.0 beta is fine; you’ll be on stable by mid-March.
  • Existing TS 4.9 project with no decorator dependencies: upgrade once 5.0 is stable, get the perf wins.
  • Existing project using NestJS or other framework relying on legacy decorators: hold on 4.9 until your framework has 5.0 support announced.

For library authors: upgrade your dev dependencies and test against 5.0 beta now. Your consumers will start landing on 5.0 by April. You don’t want to be the last library blocking upgrades.

Common Pitfalls

  • Mixing new and old decorators in the same project. They use the same @ syntax but are incompatible. Don’t try to run with both flags on.
  • Assuming const parameters fix all inference issues. They only narrow primitives and literals. For complex object shapes you still need satisfies or explicit annotations.
  • Switching to moduleResolution: "bundler" on a Node backend. This breaks Node’s actual resolution at runtime. Use nodenext for backend.
  • Upgrading without checking your linter rules. ESLint plugin updates lag TypeScript releases by a few weeks. Expect a couple of eslint-plugin-typescript errors on freshly-upgraded projects.
  • Treating 5.0 beta as production-ready. It’s beta. The stable is days away. Don’t pin a public library to 5.0 beta — wait for stable.

Wrapping Up

TypeScript 5.0 is the kind of release where the headline feature (decorators) is less impactful than the small ones (const parameters, bundler resolution, speed). The community will be on 5.0 by Q2 of 2023. Worth tracking even if you don’t upgrade immediately.

That closes the February series. March pivots hard: Go microservices, gRPC, and the performance-first backend stack that’s been quietly winning enterprise systems while everyone watched TS adoption charts.

External reference: the official 5.0 beta announcement.