Why TypeScript 5.x Changes Everything
TypeScript 5.0 shipped with over 50% faster build times, a complete rewrite of the decorator system, and a collection of type-level features so expressive they eliminate entire categories of runtime bugs. If you are still writing TypeScript the same way you did in 4.x, you are leaving correctness and developer productivity on the table.
This article breaks down each major TypeScript 5.x feature with real production code, explains the underlying type theory, and shows you exactly where these features matter in enterprise-scale codebases at companies like Airbnb, Stripe, and Microsoft.
1. The satisfies Operator — Validate Without Widening
Before satisfies, you had two unsatisfying options. You could use a type annotation and lose narrow inference, or use as const and lose type validation. The satisfies operator gives you both simultaneously.
The Classic Problem
// Old approach — annotation widens the type
type Colors = "red" | "green" | "blue";
type RGB = [number, number, number];
const palette: Record<Colors, string | RGB> = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
};
// palette.red is now string | RGB — TypeScript lost the tuple info!
// Error: Property 'map' does not exist on type 'string | RGB'
palette.red.map(x => x * 2);
The satisfies Solution
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Record<Colors, string | RGB>;
// TypeScript KNOWS palette.red is [number,number,number]
palette.red.map(x => x * 2); // Works perfectly
palette.green.toUpperCase(); // Works — still a string
// Misspellings caught at compile time:
const bad = { red: [255,0,0], purpl: "#ff00ff" } satisfies Record<Colors, string | RGB>;
// Error: Object literal may only specify known properties
Real-World: Type-Safe Route Config
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type RouteConfig = { method: HttpMethod; path: string; auth: boolean };
const routes = {
listUsers: { method: "GET", path: "/users", auth: true },
createUser: { method: "POST", path: "/users", auth: true },
healthCheck: { method: "GET", path: "/health", auth: false },
deleteUser: { method: "DELETE", path: "/users/:id", auth: true },
} satisfies Record<string, RouteConfig>;
// TypeScript infers literal types
type ListUsersMethod = typeof routes.listUsers.method; // "GET"
2. Const Type Parameters — Infer Literal Types in Generics
TypeScript normally widens generic type inference. Pass "hello" to T extends string and T becomes string, not "hello". Const type parameters fix this at the call site.
// Without const — T widens to string
function identity<T extends string>(val: T): T { return val; }
const x = identity("hello"); // type is string
// With const — T stays literal
function identityConst<const T extends string>(val: T): T { return val; }
const y = identityConst("hello"); // type is "hello"
Practical: Type-Safe Event Emitter
type EventMap = { click: MouseEvent; keydown: KeyboardEvent; load: Event };
function on<const K extends keyof EventMap>(
event: K,
handler: (e: EventMap[K]) => void
): void {
document.addEventListener(event, handler as EventListener);
}
on("click", (e) => {
console.log(e.clientX, e.clientY); // TypeScript knows this is MouseEvent
});
3. Decorator Standard — ECMAScript Stage 3
TypeScript 5.0 implements the finalized ECMAScript decorator proposal. The old experimental decorators are incompatible with the new standard.
| Feature | TypeScript 4.x | TypeScript 5.x |
|---|---|---|
| Stage | Stage 2 (experimental) | Stage 3 (standard) |
| Parameter Decorators | Supported | Removed |
| Metadata API | reflect-metadata | TC39 decorator metadata |
| Return value | Ignored for classes | Replaces decorated value |
function logged(target: Function, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
return function (this: unknown, ...args: unknown[]) {
console.log(`[${methodName}] called with:`, args);
const result = target.apply(this, args);
console.log(`[${methodName}] returned:`, result);
return result;
};
}
class UserService {
@logged
findById(id: string): { id: string; name: string } {
return { id, name: "Kashinath" };
}
}
const svc = new UserService();
svc.findById("u-123");
// [findById] called with: ["u-123"]
// [findById] returned: { id: "u-123", name: "Kashinath" }
4. Branded Types — Zero-Cost Runtime Validation
Primitive types like string are structurally equivalent in TypeScript. A UserId and an OrderId are both strings. Branded types add a nominal tag to prevent accidental misuse at zero runtime cost.
type Brand<T, B> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
const UserId = (id: string): UserId => id as UserId;
const OrderId = (id: string): OrderId => id as OrderId;
function getUser(id: UserId): { id: UserId; name: string } {
return { id, name: "Kashinath" };
}
const uid = UserId("u-123");
const oid = OrderId("o-456");
getUser(uid); // Correct
getUser(oid); // Compile error: 'OrderId' not assignable to 'UserId'
getUser("u-raw"); // Compile error: 'string' not assignable to 'UserId'
5. Template Literal Types
type EventName = "click" | "focus" | "blur";
type ListenerName = `on${Capitalize<EventName>}`;
// Result: "onClick" | "onFocus" | "onBlur"
type HttpVerb = "GET" | "POST" | "PUT" | "DELETE";
type Resource = "User" | "Post" | "Comment";
type ApiRoute = `${Lowercase<HttpVerb>}${Resource}`;
// 12 variants: "getUser" | "getPost" | ... | "deleteComment"
6. Performance Comparison
| Project Size | TS 4.9 Build | TS 5.0 Build | Speedup |
|---|---|---|---|
| Small (~1k files) | 12s | 9s | 25% |
| Medium (~10k files) | 95s | 68s | 28% |
| Large (~50k files) | 480s | 310s | 35% |
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"strict": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true
}
}
Pro tip: Enable one strict flag at a time and fix the errors before moving to the next. This keeps PRs reviewable and avoids a 300-error sea of red.