TypeScript 5.x Features Every Developer Should Be Using
·PkgPulse Team
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 consttype parameters (5.0): infer literal types withoutas consteverywhere- Variadic tuple improvements (5.2):
usingandawait usingfor automatic resource cleanup - TypeScript performance (5.x): 10-25% faster type checking on large projects
satisfiesoperator (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
}
}
Compare TypeScript tooling and related package health at PkgPulse.
See the live comparison
View typescript vs. javascript on PkgPulse →