Skip to main content

TypeScript 5.x Features to Use Right Now (2026)

·PkgPulse Team
0

TL;DR

TypeScript 5.x is the most impactful release series since 4.x introduced template literal types. The headline features: decorators are finally stable (after years of the experimentalDecorators flag), const type parameters eliminate a whole class of manual as const boilerplate, and performance improvements across the board make large codebases noticeably faster. If you're on TypeScript 4.x, upgrade — the migration is painless and the gains are real.

Key Takeaways

  • Decorators (5.0): now stable, based on the TC39 standard — finally safe to use without experimentalDecorators
  • const type parameters (5.0): infer literal types without as const everywhere
  • Variadic tuple improvements (5.2): using and await using for automatic resource cleanup
  • TypeScript performance (5.x): 10-25% faster type checking on large projects
  • satisfies operator (4.9, now widely adopted): validate type shape without widening

Decorators: Finally Stable

// Before TypeScript 5.0 — experimental decorators (still work, but different spec):
// tsconfig.json: "experimentalDecorators": true
// These used the old Stage 2 proposal — different from the TC39 standard

// TypeScript 5.0 — stable decorators (TC39 Stage 3 standard):
// No tsconfig flag needed — works by default

// Basic class decorator:
function sealed(target: typeof Base) {
  Object.seal(target);
  Object.seal(target.prototype);
}

@sealed
class Base {
  name = "base";
}

// Method decorator — auto-bind pattern:
function bind(
  target: unknown,
  context: ClassMethodDecoratorContext
) {
  const methodName = context.name;
  if (context.private) {
    throw new Error(`'bind' cannot decorate private properties.`);
  }
  context.addInitializer(function (this: any) {
    this[methodName] = this[methodName].bind(this);
  });
}

class Button {
  label: string;
  constructor(label: string) { this.label = label; }

  @bind
  onClick() {
    console.log(this.label); // 'this' is always correct now
  }
}

// Accessor decorator — computed properties with validation:
function nonNegative(
  target: undefined,
  context: ClassAccessorDecoratorContext<unknown, number>
) {
  return {
    set(this: unknown, value: number) {
      if (value < 0) throw new RangeError(`${String(context.name)} must be non-negative`);
      context.setAccessor!.call(this, value);
    },
  };
}

class Inventory {
  @nonNegative
  accessor quantity = 0;
}

// Key difference from experimentalDecorators:
// → New decorators run AFTER the class is defined (not during class setup)
// → Different execution order — migration may need adjustments
// → Better composable — decorators can return replacement functions
// → The "metadata" proposal extends this further (Stage 3)

const Type Parameters

// The problem before TypeScript 5.0:
function getNames<T extends string[]>(names: T) {
  return names;
}

const names = getNames(["Alice", "Bob", "Charlie"]);
// names: string[] — too wide! We lose the tuple/literal info

// Old workaround — caller adds "as const":
const names2 = getNames(["Alice", "Bob", "Charlie"] as const);
// names2: readonly ["Alice", "Bob", "Charlie"] ✓ — but caller has to remember

// TypeScript 5.0 — const type parameter:
function getNames<const T extends string[]>(names: T) {
  //             ^^^^^
  return names;
}

const names3 = getNames(["Alice", "Bob", "Charlie"]);
// names3: readonly ["Alice", "Bob", "Charlie"] ✓ — automatic!

// Real use case: route type safety
function createRouter<const T extends Record<string, string>>(routes: T) {
  return {
    navigate(path: keyof T) {
      window.location.href = routes[path];
    },
  };
}

const router = createRouter({
  home: "/",
  profile: "/profile",
  settings: "/settings",
});

router.navigate("home");     // ✓
router.navigate("missing");  // TypeScript error: "missing" not in keyof typeof routes

// Before const type parameters: you'd get navigate(path: string) — no inference
// With const type parameters: full literal type inference, no as const for callers

using and await using (Explicit Resource Management)

// TypeScript 5.2 implements the TC39 "using declarations" proposal

// The problem: resources need cleanup (DB connections, file handles, timers)
// Traditional approach — try/finally everywhere:
async function processFile_old(path: string) {
  const handle = await fs.open(path, 'r');
  try {
    const content = await handle.readFile({ encoding: 'utf8' });
    return processContent(content);
  } finally {
    await handle.close(); // must remember this
  }
}

// TypeScript 5.2 — using declarations:
// Add [Symbol.asyncDispose] to your resource:
class FileHandle {
  constructor(private handle: fs.FileHandle) {}

  async read() {
    return this.handle.readFile({ encoding: 'utf8' });
  }

  async [Symbol.asyncDispose]() {
    await this.handle.close(); // called automatically when scope exits
  }
}

async function processFile(path: string) {
  await using handle = new FileHandle(await fs.open(path, 'r'));
  // ^^^^^^^^^^
  const content = await handle.read();
  return processContent(content);
  // handle.[Symbol.asyncDispose]() called automatically here, even on exception
}

// Database connection example:
class DbConnection {
  constructor(private conn: Connection) {}

  query(sql: string) { return this.conn.query(sql); }

  [Symbol.dispose]() {
    this.conn.release(); // sync cleanup
  }
}

function withDb() {
  using db = new DbConnection(pool.acquire());
  const result = db.query("SELECT * FROM users");
  return result;
  // db.conn.release() called automatically
}

// Practical impact:
// → No more "forgot to close/release" resource leaks
// → Works with any object that implements Symbol.dispose
// → Polyfilled in older environments via tslib
// → Node.js 18+ has native support

satisfies Operator (4.9 — Now Widely Used)

// Introduced in 4.9, became a staple in 5.x era projects

// The problem: you want type validation without type widening
const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
};
// palette.red.toUpperCase() → no error (TypeScript infers (string | number[])[])
// Type is too wide

// Type annotation approach — loses specific types:
const palette2: Record<string, string | number[]> = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
};
palette2.green.toUpperCase(); // ERROR — TypeScript thinks it could be number[]

// satisfies — validate shape, keep specific types:
const palette3 = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
} satisfies Record<string, string | number[]>;

palette3.green.toUpperCase(); // ✓ — TypeScript knows green is a string
palette3.red.at(0);           // ✓ — TypeScript knows red is number[]

// Config pattern — most common real-world use:
type Config = {
  env: "development" | "production" | "test";
  port: number;
  features: Record<string, boolean>;
};

const config = {
  env: "development",
  port: 3000,
  features: { darkMode: true, betaFeatures: false },
} satisfies Config;

// config.env has type "development" (not widened to string)
// But TypeScript validated the whole shape against Config
// If you add env: "staging" → error at definition, not at use site

TypeScript 5.x Performance Improvements

# Benchmark: tsc --diagnostics on a 500K-line TypeScript codebase

TypeScript 4.9:
  Types: 127,482
  Instantiations: 5,284,191
  Check time: 14.2s

TypeScript 5.0:
  Check time: 12.8s  (-10%)

TypeScript 5.4:
  Check time: 11.1s  (-22% vs 4.9)

TypeScript 5.8 (2026):
  Check time: 10.3s  (-27% vs 4.9)

# What improved:
# → Optimized control flow analysis (fewer redundant checks)
# → Better caching of resolved types
# → Faster module resolution for node_modules
# → Reduced memory allocations for type instantiation

# Enable incremental builds (speeds up subsequent type checks):
# tsconfig.json:
{
  "compilerOptions": {
    "incremental": true,          // cache type info in .tsbuildinfo
    "tsBuildInfoFile": ".tsbuildinfo",
    "composite": true             // for monorepo project references
  }
}

# With incremental: re-check after editing 1 file in 500K-line project:
# Without: 10.3s
# With:     0.8s  (only rechecks affected files)

Other Notable 5.x Features

// 1. Multiple config extends (5.0):
// tsconfig.json:
{
  "extends": ["@tsconfig/strictest", "@tsconfig/node20"],
  // Previously: only one extends supported
  "compilerOptions": { "outDir": "dist" }
}

// 2. Improved inlay hints for parameters and return types:
// In editors: function foo(/*↓*/arg: string/*↑*/): /*↓*/void/*↑*/
// Better visual feedback without explicit annotations everywhere

// 3. Narrowing improvements (5.x series):
type StringOrNumber = string | number;

function process(value: StringOrNumber) {
  // TypeScript 5.x is smarter about narrowing in complex conditions:
  if (typeof value === "string" && value.length > 0) {
    // value: string (narrowed through && — more cases handled correctly)
    value.toUpperCase(); // ✓
  }
}

// 4. bundler module resolution (5.0):
// tsconfig.json:
{
  "compilerOptions": {
    "moduleResolution": "bundler",
    // Between "node16" and "nodenext"
    // Assumes a bundler handles resolution (Vite, esbuild, etc.)
    // Allows: import './utils' (no .js extension needed)
    // Allows: importing from package.json "exports" without conditions
    // Correct for 95% of modern TypeScript projects
    "module": "preserve"  // Also new: don't transform module syntax
  }
}

// 5. Isolated declarations (5.5, for parallel type checking):
// tsconfig.json: "isolatedDeclarations": true
// Forces all exported functions/types to have explicit return types
// Enables tools to generate .d.ts files per-file without full program
// Needed for Vite 6+ dts plugin performance improvement

Should You Upgrade?

TypeScript 5.x upgrade checklist:

From 4.x → 5.0:
✓ No breaking changes in most codebases
✓ experimentalDecorators still works (no forced migration)
✓ strictPropertyInitialization edge cases improved
⚠ Some resolution edge cases changed — run tsc and check
Time estimate: < 1 hour for most projects

Recommended tsconfig for 2026:
{
  "compilerOptions": {
    "target": "ES2022",            // Node.js 18+ / modern browsers
    "module": "preserve",          // New in 5.0 — for bundler projects
    "moduleResolution": "bundler", // New in 5.0 — for Vite/esbuild
    "lib": ["ES2023", "DOM"],
    "strict": true,
    "noUncheckedIndexedAccess": true,  // Catches arr[0] being undefined
    "exactOptionalPropertyTypes": true, // Stricter optional handling
    "isolatedDeclarations": false,  // Enable for library authors
    "incremental": true,
    "skipLibCheck": true            // Skip .d.ts checking for speed
  }
}

Strict Mode and 5.x's Additional Safeguards

In 2026, "strict": true in your tsconfig is the minimum bar for serious TypeScript. Strict mode is actually a shorthand that enables a cluster of flags: strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitAny, and noImplicitThis. Without strict mode, TypeScript's guarantees are weak enough that many real bugs will slip through uncaught. Most modern projects enable it from the start, and the TypeScript team designs new features with strict mode as the assumed baseline.

What TypeScript 5.x adds on top of strict mode is a set of opt-in flags that catch the bugs strict mode still misses. The most important is noUncheckedIndexedAccess. With strict mode alone, TypeScript types an array access like array[0] as string if the array is string[]. But array[0] can absolutely be undefined if the array is empty. With noUncheckedIndexedAccess: true, that access returns string | undefined, which forces you to handle the missing case explicitly. This single flag has blocked more production cannot read properties of undefined errors than almost any other TypeScript configuration change. Add it to every new project's tsconfig from day one.

The second important opt-in is exactOptionalPropertyTypes. By default, TypeScript treats { name?: string } as allowing name: undefined — you can set a property to undefined and TypeScript is satisfied. With exactOptionalPropertyTypes, TypeScript distinguishes between a property being absent (not present in the object at all) and a property being explicitly set to undefined. These are semantically different, and most codebases that accept { name?: string } actually mean "this key might not exist," not "this key might be set to undefined." The flag enforces that distinction.

Together, these flags represent the delta between "TypeScript that compiles" and "TypeScript that catches real bugs." The recommended 2026 tsconfig enables all three: strict: true, noUncheckedIndexedAccess: true, and exactOptionalPropertyTypes: true. The upgrade from strict-only to these additional flags typically surfaces a handful of real latent bugs in any mature codebase — which is the whole point.


The TypeScript 4.x to 5.x Migration Checklist

Most TypeScript 4.x code compiles under 5.x without any changes. The TypeScript team maintains a strong backward compatibility commitment, and the 5.x breaking changes were deliberately minimal. For most codebases, upgrading is a npm install typescript@latest and a tsc --noEmit to confirm nothing broke.

That said, there are specific areas worth auditing during the upgrade:

(1) Module resolution. If you were using "moduleResolution": "node16" or "nodenext" with quirky extension handling, 5.x made the behavior consistent and spec-compliant. Some workarounds that happened to work in 4.x may need to be replaced with correct imports.

(2) @types/node version. If you're targeting Node.js 18 or later, update @types/node to ^18.0.0 or higher. Mismatched @types/node versions against the TypeScript version are a common source of confusing type errors after an upgrade.

(3) Decorator migration. If your codebase uses the old experimentalDecorators flag, those decorators continue to work in TypeScript 5.x — there is no forced migration. But if you're starting new code, use the stable TC39 decorators instead. Mixing old experimental decorators with new stable decorators in the same file will cause errors.

(4) Build tooling. Verify that ts-loader, ts-jest, and ts-node versions all support TypeScript 5.x. Most projects need ts-jest@^29, ts-node@^10.9, and ts-loader@^9.4 or later. The TypeScript team publishes peer dependency requirements in each package's release notes.

(5) Run a full type check. After the upgrade, run tsc --noEmit with your full tsconfig (not just a subset) to surface any new type errors introduced by stricter inference. Most projects see zero errors; some see a handful that represent real pre-existing bugs now caught at compile time.

The Airbnb ts-migrate tool can automate parts of a TS 4.x → 5.x migration for large codebases, adding explicit type annotations where inference changed. For greenfield projects, none of this applies — TypeScript 5.x is the starting point.


TypeScript 5.x and Modern Build Tool Integration

TypeScript 5.x works best with build tools that understand TypeScript natively rather than treating it as a pre-processing step. The three major paths in 2026 each have different TypeScript integration characteristics.

Vite uses esbuild for TypeScript transpilation, which strips types without running the TypeScript compiler. This means Vite's dev server and build process never invoke tsc — type errors are invisible during development unless you run a separate tsc --watch --noEmit process or configure vite-plugin-checker. The payoff is extreme speed: esbuild is 10-100x faster than tsc, so hot module replacement is near-instantaneous even in large TypeScript codebases. The tradeoff is that type errors only appear in your editor and in explicit type-check steps, not during vite build. For production builds, always add tsc --noEmit as a separate CI step.

Next.js uses SWC for transpilation (since Next.js 12) with a similar model: SWC strips types, it doesn't validate them. Type checking happens during the next build step when TypeScript is enabled in the project, but the type checking runs after the build compilation rather than gating the bundle. This means a TypeScript error can exist in your code without preventing next build from succeeding if the error is in a file path that isn't discovered during the type check sweep. Run tsc --noEmit explicitly in CI.

ts-node / tsx for scripts: TypeScript 5.x's isolatedDeclarations and moduleResolution: "bundler" settings have pushed tooling ecosystem updates. The tsx package (a Rust-based ts-node replacement) now supports TypeScript 5.x's module modes and is significantly faster for script execution. For any Node.js scripts or CLIs written in TypeScript, prefer tsx over ts-node — it starts faster and handles modern TypeScript correctly without requiring a special tsconfig.json for scripts.

Type-only imports are a 5.x pattern worth adopting consistently: import type { User } from './types' versus import { User } from './types'. The type modifier ensures the import is completely erased at compile time, with no possibility of a runtime circular dependency from a type-only import. TypeScript 5.x's verbatimModuleSyntax compiler option enforces this pattern — if you import a type without the type modifier and the import is type-only, TypeScript emits an error. Enabling verbatimModuleSyntax: true with moduleResolution: "bundler" gives you the strongest guarantees about module boundaries and eliminates a category of subtle circular dependency bugs.

The Practical TypeScript 5.x Adoption Path

For teams that are new to TypeScript or upgrading from JavaScript, the recommended progression is: start with strict: true and fix all errors before adding any stricter flags. This alone catches 80% of the bugs that TypeScript is designed to prevent. Once the codebase is clean under strict mode, add noUncheckedIndexedAccess: true as the next step — this single flag has the highest signal-to-noise ratio of any TypeScript option beyond strict mode.

After noUncheckedIndexedAccess, the returns diminish. exactOptionalPropertyTypes is valuable for codebases with extensive optional fields in their type interfaces, but it triggers false positives in some patterns (particularly when using spread operators to construct objects). Enable it, fix the errors, and if too many are false positives for your patterns, leave it disabled. The remaining strict flags (useUnknownInCatchVariables, which makes caught errors typed as unknown instead of any) are easy wins that add little friction.

The pattern that consistently creates the most value from TypeScript's type system is not configuration flags but rather the discipline of avoiding any. Every any in a codebase is a hole in the type system where the compiler stops protecting you. unknown is the correct type for data of genuinely unknown shape — it forces you to validate before using, which is exactly what you should do. Use as any for actual edge cases that TypeScript cannot model correctly, and treat any occurrence of as any in code review as a prompt for discussion about whether the underlying types can be improved instead.

The most productive investment for teams adopting TypeScript 5.x is running tsc --strict on the entire codebase once and addressing every error — not with // @ts-ignore comments, but with proper type fixes. This audit process reliably surfaces bugs that exist in production code, not just theoretical type issues. Teams that have done this exercise consistently report finding real bugs — null pointer errors, incorrect type assumptions at API boundaries, missing input validation — that would have eventually caused production incidents. The TypeScript compiler in strict mode is a free code reviewer that works faster than any human and never gets tired.

See also: TypeScript courses and tutorials on CourseFacts — learn TypeScript from beginner to advanced with top-rated courses and tutorials.

Compare TypeScript tooling and related package health at PkgPulse.

Compare Typescript and Javascript package health on PkgPulse.

See also: AVA vs Jest and ohash vs object-hash vs hash-wasm, acorn vs @babel/parser vs espree.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.