TypeScript releases features faster than most developers can adopt them. Every six months there's a new blog post about decorators, satisfies operators, or some new utility type. But when you're building real products, only a handful of features actually change how you work day to day.
Here's what's been worth learning from TypeScript 5.x, based on a year of using it in production.
The satisfies Operator (Game Changer)
This landed in 4.9 but most people only started using it properly in 5.x. The problem it solves: you want to type-check an object against a type without widening its type. Classic example:
// Without satisfies - type is Record<string, string | number>
const config: Record<string, string | number> = {
port: 3000,
host: "localhost"
};
config.port.toFixed(); // Error: property doesn't exist on string | number
// With satisfies - type is { port: number, host: string }
const config = {
port: 3000,
host: "localhost"
} satisfies Record<string, string | number>;
config.port.toFixed(); // Works! TypeScript knows it's a number
In practice, I use satisfies for plan configurations, route definitions, and any constant object where I want both validation and narrow types.
Const Type Parameters
TypeScript 5.0 introduced const type parameters. Before this, passing a literal to a generic function widened it. Now you can preserve the literal type:
function createRoute<const T extends string>(path: T) {
return { path };
}
const route = createRoute("/api/users");
// route.path is "/api/users", not string
This is invaluable for builder patterns, configuration objects, and anything where you want the exact literal type preserved through function calls.
Decorators (Finally Standard)
TypeScript 5.0 shipped TC39 standard decorators. If you've been using experimental decorators (the ones that need experimentalDecorators in tsconfig), the new ones are different. They're stage 3, meaning they'll work natively in JavaScript eventually.
For most projects in 2026, you'll encounter decorators in frameworks (NestJS, Angular) rather than writing your own. But if you do need them, the new syntax is cleaner and doesn't require reflect-metadata.
Verbatim Module Syntax
The verbatimModuleSyntax flag in tsconfig replaces the confusing importsNotUsedAsValues and preserveValueImports flags. It's simple: if you import a type, use import type. If you don't, TypeScript errors.
import type { User } from './types'; // Type-only import, erased at runtime
import { getUser } from './db'; // Value import, kept at runtime
This matters for bundler performance. Type-only imports get erased completely, reducing bundle size and improving tree-shaking. Enable it in every new project.
Module Resolution: Bundler Mode
Setting moduleResolution: "bundler" in tsconfig tells TypeScript to resolve modules the way modern bundlers do. This means you can import from package.json exports fields, use the .js extension for TypeScript files (as ESM requires), and generally stop fighting with module resolution.
If you're using Next.js, Vite, or any modern bundler, switch to bundler resolution. It eliminates an entire category of "works in dev but not in build" issues.
Using Multiple Config Files
TypeScript 5.x improved config file extends. You can now extend from multiple configs:
// tsconfig.json
{
"extends": ["@tsconfig/strictest", "./tsconfig.paths.json"],
"compilerOptions": { ... }
}
For Next.js projects, I keep a base config with strict settings and a separate paths config. The build system (Next.js) merges them correctly.
Template Literal Types in Practice
Template literal types have been around since 4.1, but they've gotten more practical with improved inference in 5.x. Real-world use case - typing API routes:
type ApiRoute = `/api/${string}`;
type PlanId = "free" | "flame" | "blaze" | "neon";
type PlanRoute = `/api/plans/${PlanId}`;
function fetchPlan(route: PlanRoute) { ... }
fetchPlan("/api/plans/flame"); // OK
fetchPlan("/api/plans/gold"); // Error
What I Skip
Not every new feature is worth adopting. Things I've tried and stepped back from:
- Project references: Great in theory for monorepos, painful in practice with Next.js. I use path aliases instead.
- Strict null checks on legacy code: If you're adding TypeScript to an existing project, enable strict incrementally. Turning on every flag at once generates hundreds of errors that don't represent real bugs.
- Complex conditional types: If your type definition needs more than two lines of conditional logic, you've probably over-engineered it. Use a simpler type and a runtime check.
The Pragmatic TypeScript Setup
For a new project in 2026, here's what I enable:
{
"compilerOptions": {
"strict": true,
"verbatimModuleSyntax": true,
"moduleResolution": "bundler",
"target": "ES2022",
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true
}
}
The noUncheckedIndexedAccess flag is the unsung hero. It makes array[0] return T | undefined instead of T, catching a huge category of runtime errors at compile time.
RAXXO Studio is built with TypeScript 5 on Next.js. Check it out at studio.raxxo.shop.