TypeScript 5.x: The Features That Make Type-Safe Code Actually Enjoyable
import Post from β../../layouts/Post.astroβ; export const prerender = true;
TypeScript 5.x brought a wave of improvements that make type-safe code actually enjoyable to write β not just theoretically correct.
1. Const Type Parameters
The generic constraint nobody knew they needed:
// Before: inference leaks beyond intended scope
function asArray<T>(value: T): T[] {
return [value];
}
const result = asArray("hello"); // type: string[]
const force = asArray("hello" as const); // type: readonly ["hello"]
// After: const type parameter forces the inference you want
function asConstArray<const T>(value: T): readonly T[] {
return [value];
}
const result = asConstArray("hello"); // type: readonly ["hello"]
const result2 = asConstArray([1, 2, 3]); // type: readonly number[]
This is huge for builder patterns, configuration objects, and any API where you want exact inference.
2. Decorators (Stable)
TypeScript 5.0 stabilized the ECMAScript decorators spec:
// Method decorator
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${key} with`, args);
return original.apply(this, args);
};
return descriptor;
}
class UserService {
@log
createUser(name: string, email: string) {
// ...
}
}
No experimentalDecorators flag needed anymore. This works in Node 20+ and modern bundlers.
3. using Declarations (Resource Management)
Like await using in C#, automatic cleanup for resources:
// Auto-closes when scope ends
async function processFile(path: string) {
const resource = await using file = await openFile(path);
// work with file
} // file.close() called automatically, even on error
If youβre coming from Pythonβs with or C#βs using, this finally brings it to TypeScript.
4. satisfies Operator β The Best Feature Nobody Talks About
// Before: union types lose specificity
const config = {
port: 3000,
host: "localhost",
features: ["auth", "caching"],
};
config.port = "3000"; // no error? wrong!
// After: validate AND keep inference
const config = {
port: 3000,
host: "localhost",
features: ["auth", "caching"],
} satisfies Config;
config.port = "3000"; // β
Error: string not assignable to number
config.features.push("logging"); // β
Fine β it's still string[]
satisfies validates against the type but preserves the literal inference. Game changer for configuration objects.
5. Type Parameters in Template Literal Types
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type HoverEvent = EventName<"hover">; // "onHover"
// Complex example: type-safe route params
type Route<T extends string> = `/api/${T}`;
type UserRoute = Route<"users">; // "/api/users"
type PostRoute = Route<"posts/:id">; // "/api/posts/:id"
6. Variadic Tuple Types β Finally Readable
// Before: ugly rest/spread
function merge<T extends any[], U extends any[]>(a: T, b: U): [...T, ...U] {
return [...a, ...b];
}
// After: cleaner with explicit tuple labels
function merge<First extends unknown[], Second extends unknown[]>(
a: First,
b: Second
): [...First, ...Second] {
return [...a, ...b];
}
const merged = merge(["a", "b"], [1, 2]);
// type: [string, string, number, number] β not (string | number)[]
The One to Actually Learn First
If you only pick one from this list: satisfies. Itβs the most immediately useful, works in existing codebases without refactoring, and prevents a whole class of bugs in configuration objects.
// Every config object in your codebase should use this
const db = {
host: "localhost",
port: 5432,
ssl: false,
} satisfies DatabaseConfig; Enjoying the content? Here are tools I personally use and recommend:
- π Hosting: Bluehost β what this blog runs on
- π Tech Gear: My Amazon Store β keyboards, monitors, dev tools I use
Purchases through my links help keep this blog ad-free π
Enjoyed this post?
Subscribe to the newsletter or follow on YouTube for more dev content.
π¬ Watch Shorts