TypeScript 5.8 Features: 3 Worth Using, 2 to Ignore

2026-05-31 · Nico Brandt

TypeScript 5.8 shipped in February, your team Slack has three links to it, and your tsconfig.json hasn’t been touched since 5.5. Every page-one article walks you through every feature with the same earnest weight. That’s not what you need on a Monday.

You need a verdict. Three TypeScript 5.8 features are worth touching your config for. Two are noise unless you have a specific reason to care. And one of those three quietly buys you a discount on the much bigger upgrade landing later this year — the one nobody framing 5.8 as a routine release is talking about yet.

Why Most TypeScript 5.8 Coverage Is Useless

The official announcement is comprehensive and well written. It is also a changelog. Every flag, every behavioral tweak gets the same paragraph of patient explanation — the right call for a release post, the wrong call for a working developer at standup. The other page-one articles mostly paraphrase it. None tells you, “open your tsconfig, add these two lines, ignore the rest.”

So that’s what this is — a code review of the release, not a feature tour. By the end you’ll have a copy-paste config, a five-minute upgrade checklist, and a clear answer to whether any of this matters in 2026. Three to adopt. Two to skip. One block to paste.

Let’s start with the one that actually changes how you write code.

Worth Using #1: –erasableSyntaxOnly

This is the headline feature, and the reason isn’t really about TypeScript — it’s about Node.js.

Since Node.js 22.18 (July 2025), the runtime executes .ts files natively. No ts-node, no tsx, no bundler — node app.ts just runs. The catch: Node doesn’t compile, it strips. Type annotations come off, but anything that emits runtime code — enums, namespaces with values, constructor parameter properties, import = aliases, decorators — survives the strip and breaks.

--erasableSyntaxOnly is the compiler flag that refuses to let you write any of that in the first place. It’s a guarantee: every line you author is something a stripping runtime can handle.

Three refactor patterns cover almost every error it’ll throw at you.

Enum to as const object. This is the cleanest swap:

// before
enum Status { Active = "active", Banned = "banned" }

// after
const Status = { Active: "active", Banned: "banned" } as const;
type Status = (typeof Status)[keyof typeof Status];

Same call site (Status.Active), same type safety, fully erasable.

Parameter properties to explicit fields. Constructor shorthand was nice. It also wasn’t erasable:

class User {
  constructor(public name: string, public email: string) {}
}
// becomes
class User {
  name: string;
  email: string;
  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }
}

More lines, same shape. If you’re shipping TypeScript that needs to run with no build step, this is the trade.

Namespace to ES module. If you still have namespace Foo { export function bar() {} } lying around, it’s been time for years. Move the contents into a module and import normally.

Pair the flag with verbatimModuleSyntax: true so imports behave the way Node expects. Know when not to use it: NestJS leans on decorators, TypeORM leans on parameter properties, and library authors shipping decorators can’t adopt this without a backward-compat story — see the three decorator patterns worth migrating to for the full migration story. For app code that runs on Node, it’s a near-universal win. How to actually use TypeScript without hating it covers the patterns this builds on.

That’s the obvious headline. The next feature is the quiet one that catches a bug you didn’t know you had.

Worth Using #2: Granular Return-Expression Checks

This one ships with no flag, no opt-in, no migration. You upgrade and start seeing errors you probably should have been seeing for years.

Here’s the bug. A function with a declared return type, a ternary, and one branch that produces any — usually from JSON.parse, an untyped library, or an as any someone left for “just a minute” three releases ago:

function getCount(input: string): number {
  return input ? JSON.parse(input) : 0;
}

Before 5.8, JSON.parse(input) returns any, and any infects the entire ternary. The function reports its return type as number — except it doesn’t, really. It returns whatever JSON.parse hands back. Callers do arithmetic on what might be a string. Production discovers this at 2 a.m.

After 5.8, the compiler checks each branch of the conditional against the declared return type. The JSON.parse branch fails. The fix is usually one line: hoist the unsafe value into a typed local, and the unsafe assignment lives at the source instead of leaking downstream.

Pure upside. The only cost is honesty about a class of bug your codebase has been quietly hiding. Paired with satisfies operator patterns, it catches most of the silent any propagation you have left.

That’s the silent-bug feature. The third one is narrower — but if you ship a library, it matters.

Worth Using #3: require() of ESM Under –module nodenext

Audience check: if you ship a CommonJS library and your consumers want ESM-only dependencies, read this. If you write apps, skim and move on.

Under --module nodenext in TypeScript 5.8, require() of an ESM module is allowed. The compiler stopped objecting because Node.js 22 stopped objecting at runtime — and Node.js 26 stabilized require(esm) for good measure. For library authors who’ve been writing brittle dual-package gymnastics, this removes one layer of build-time friction.

The gotcha is small but real: this only works under --module nodenext. --module node18 and --module node16 still block it. If you’ve been pinning to node18 to avoid nodenext’s evolving behavior, you’ll need to revisit that choice when you adopt the change.

One config line. Narrow audience. But for the people it serves, it’s been overdue for a year.

That’s three worth using. Now the two flags you can comfortably ignore.

Skip These: –module node18 and –libReplacement

--module node18 exists for a specific reader: you’re locked to Node 18 LTS, you don’t want nodenext’s changing semantics across minor versions, and you need module-resolution behavior frozen to the Node 18 era. That’s a narrow situation. Most teams should be on nodenext and tracking Node’s actual behavior, not a snapshot of it.

--libReplacement is a build-performance optimization for monorepos that ship custom lib shims to override the standard library types. If you’ve never written a lib.es5.d.ts replacement, you don’t need this flag. The default behavior is fine. Even the official announcement frames it as an opt-in for advanced setups.

Preserved computed property names in .d.ts files gets the one sentence it deserves: a niche declaration-emitter improvement that keeps [Symbol.iterator] and similar computed keys intact during emit. If you don’t author libraries with computed keys, you’ll never notice.

Two flags. One emitter tweak. Skip them with a clear conscience.

You now know what to keep and what to skip. Here’s the actual config.

The tsconfig.json You Came For (Plus a 5-Minute Upgrade Checklist)

{
  "compilerOptions": {
    "target": "ES2022",              // modern Node + browsers, drop pre-ES2022 polyfills
    "module": "nodenext",            // unlocks require() of ESM + future-proof
    "moduleResolution": "nodenext",  // must match module for nodenext semantics
    "erasableSyntaxOnly": true,      // forbids non-strippable syntax (the big one)
    "verbatimModuleSyntax": true,    // imports survive type stripping correctly
    "strict": true,                  // table stakes; not 5.8-specific but assumed below
    "skipLibCheck": true             // sanity for monorepos with heterogeneous deps
  }
}

That’s the entire config delta. Two new lines (erasableSyntaxOnly, verbatimModuleSyntax), and three that you may already have set. No magic.

The upgrade checklist:

  1. Bump TypeScript. npm install --save-dev typescript@^5.8. Run npx tsc --version to confirm.
  2. Add erasableSyntaxOnly: true. Run tsc --noEmit. Every error points at an enum, a namespace with runtime code, a parameter property, an import = alias, or a decorator. Use the three refactor patterns from earlier.
  3. Fix the new return-type errors. Those weren’t there in 5.7. They’re real. Hoist the unsafe value into a typed local and the error usually disappears.
  4. Set module and moduleResolution to nodenext. Especially if you ship a library. This is what gets you the require() of ESM behavior — and it’s where you’ll spend the bulk of any migration time if you were on node16 or node18.
  5. Consider --isolatedDeclarations. It’s not in the snippet above because it deserves its own decision, but if your build is slow on .d.ts generation, it’s the next flag to read about after this one.

That handles Monday. Now the question worth answering: why bother in 2026 when TypeScript 7 is around the corner?

Why This Prep Pays Off in TypeScript 7

This was the question we opened with: is 5.8 worth a Monday? The honest answer is that 5.8 by itself is a perfectly fine release that catches a real bug and tightens a few seams. That alone wouldn’t be worth a deep upgrade post.

What makes it worth the time is what 5.8 sets up. Node.js 22.18+ runs erasable TypeScript natively, today, with no build step — and erasableSyntaxOnly is the compiler-enforced guarantee that your code will keep running there. TypeScript 7, the Go-rewritten compiler announced in March 2025, targets roughly a 10x speedup on codebases that don’t lean on transformative syntax. The same flag that makes Node happy makes the Go compiler happy. The teams that adopt erasableSyntaxOnly now get both wins handed to them when 7 ships. The teams that wait will be doing the same refactor under a deadline.

Three flags this week. Skip the other two with a clear conscience. Future you, doing the TypeScript 7 upgrade in an afternoon instead of a sprint, will be glad you did.