TypeScript 5.0+ Revolutionary Features
TypeScript 5.x represents a major milestone with ECMAScript Decorators, const type parameters, and significant performance improvements. This lesson explores the latest features and best practices for modern TypeScript development.
ECMAScript Decorators (Stage 3)
TypeScript 5.0 introduced support for the new ECMAScript decorators proposal, which differs from the experimental decorators:
// Enable in tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": false // Use standard decorators
}
}
Class Decorators:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class BugReport {
type = "report";
title: string;
constructor(title: string) {
this.title = title;
}
}
// Class is now sealed - cannot add properties
const report = new BugReport("Memory leak");
// report.newProperty = "value"; // Error in strict mode
Method Decorators:
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with:`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3);
// Logs:
// Calling add with: [2, 3]
// Result: 5
Accessor Decorators:
function configurable(value: boolean) {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.configurable = value;
};
}
class Point {
private _x: number;
constructor(x: number) {
this._x = x;
}
@configurable(false)
get x() {
return this._x;
}
set x(value: number) {
this._x = value;
}
}
Const Type Parameters (TypeScript 5.0)
Const type parameters preserve literal types and create more precise inference:
// Without const
function makeArray<T>(items: T[]): T[] {
return items;
}
const arr1 = makeArray([1, 2, 3]);
// Type: number[]
// With const
function makeConstArray<const T>(items: T[]): T[] {
return items;
}
const arr2 = makeConstArray([1, 2, 3]);
// Type: readonly [1, 2, 3]
Powerful for configuration objects:
function createConfig<const T extends Record<string, any>>(config: T): T {
return config;
}
const config = createConfig({
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3
});
// Type is:
// {
// readonly apiUrl: "https://api.example.com";
// readonly timeout: 5000;
// readonly retries: 3;
// }
// This enables exact type checking
function connect(url: "https://api.example.com") {
// Implementation
}
connect(config.apiUrl); // ✓ Works! Type is exact literal
The 'satisfies' Operator (TypeScript 4.9+)
Validate types without widening them:
type Color = "red" | "green" | "blue";
interface Theme {
primary: Color;
secondary: Color;
}
// Without satisfies - loses literal types
const theme1: Theme = {
primary: "red",
secondary: "blue"
};
// theme1.primary has type Color (not "red")
// With satisfies - keeps literal types
const theme2 = {
primary: "red",
secondary: "blue"
} satisfies Theme;
// theme2.primary has type "red"
// Still catches errors
const invalid = {
primary: "red",
secondary: "yellow" // Error: "yellow" not assignable to Color
} satisfies Theme;
Powerful for configuration validation:
type Route = {
path: string;
method: "GET" | "POST" | "PUT" | "DELETE";
handler: Function;
};
const routes = {
getUser: {
path: "/users/:id",
method: "GET",
handler: () => {}
},
createUser: {
path: "/users",
method: "POST",
handler: () => {}
}
} satisfies Record<string, Route>;
// Type of routes.getUser.method is "GET" (not "GET" | "POST" | "PUT" | "DELETE")
// But we still get validation that all routes match Route type
Note: Use satisfies when you want type validation without widening. Use type annotations when you want to explicitly widen types.
Using Declarations (TypeScript 5.2)
Explicit resource management with automatic cleanup:
// Declare a disposable resource
class FileHandle implements Disposable {
constructor(private path: string) {
console.log(`Opening ${path}`);
}
write(data: string): void {
console.log(`Writing to ${this.path}: ${data}`);
}
[Symbol.dispose](): void {
console.log(`Closing ${this.path}`);
}
}
// Using declaration - automatic cleanup
{
using file = new FileHandle("data.txt");
file.write("Hello, World!");
// file is automatically disposed at end of block
}
// Logs: "Closing data.txt"
Perfect for database connections:
class DatabaseConnection implements AsyncDisposable {
constructor(private connectionString: string) {}
async connect(): Promise<void> {
console.log(`Connecting to ${this.connectionString}`);
}
async query(sql: string): Promise<any[]> {
console.log(`Executing: ${sql}`);
return [];
}
async [Symbol.asyncDispose](): Promise<void> {
console.log(`Disconnecting from ${this.connectionString}`);
}
}
async function queryDatabase() {
await using db = new DatabaseConnection("localhost:5432");
await db.connect();
const users = await db.query("SELECT * FROM users");
return users;
// db is automatically disposed here
}
Import Attributes (TypeScript 5.3)
Specify import assertions for non-JavaScript modules:
// Import JSON with type assertion
import config from "./config.json" with { type: "json" };
// Import CSS modules
import styles from "./styles.css" with { type: "css" };
// Type-safe configuration
type Config = {
apiUrl: string;
timeout: number;
};
const typedConfig: Config = config;
Improved Inference for Template Strings
TypeScript 5.0+ has better inference for template literal types:
// Build type-safe route helpers
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Route = `/${string}`;
function createRoute<M extends HttpMethod, R extends Route>(
method: M,
route: R
): `${M} ${R}` {
return `${method} ${route}`;
}
const getUserRoute = createRoute("GET", "/users/:id");
// Type: "GET /users/:id"
const createUserRoute = createRoute("POST", "/users");
// Type: "POST /users"
// Type-safe route matching
function matchRoute(route: "GET /users/:id" | "POST /users") {
// Implementation
}
matchRoute(getUserRoute); // ✓ OK
matchRoute(createUserRoute); // ✓ OK
// matchRoute("GET /invalid"); // ✗ Error
Faster Type Checking Performance
TypeScript 5.0 introduced significant performance improvements:
// tsconfig.json optimizations
{
"compilerOptions": {
// Faster resolution
"moduleResolution": "bundler",
// Skip lib checking for speed
"skipLibCheck": true,
// Faster builds with incremental
"incremental": true,
// Assume changes only affect direct deps
"assumeChangesOnlyAffectDirectDependencies": true
}
}
Switch Statement Improvements
Better exhaustiveness checking in switch statements:
type Status = "pending" | "approved" | "rejected";
function handleStatus(status: Status): string {
switch (status) {
case "pending":
return "Waiting for approval";
case "approved":
return "Request approved";
case "rejected":
return "Request rejected";
// If you add a new status, TypeScript will error here
}
// TypeScript knows this is unreachable
}
// Better with exhaustive check
function handleStatusExhaustive(status: Status): string {
switch (status) {
case "pending":
return "Waiting for approval";
case "approved":
return "Request approved";
case "rejected":
return "Request rejected";
default:
// This ensures we handle all cases
const _exhaustive: never = status;
return _exhaustive;
}
}
TypeScript 5.4+ Features
NoInfer Utility Type:
// Prevent inference in specific positions
function createCollection<T>(
initial: T[],
additional: NoInfer<T>[]
): T[] {
return [...initial, ...additional];
}
const nums = createCollection([1, 2, 3], [4, 5, 6]); // ✓ OK
// const invalid = createCollection([1, 2], ["3"]); // ✗ Error
Improved Narrowing in Generic Functions:
function processValue<T>(value: T): void {
if (typeof value === "string") {
// TypeScript 5.4+ narrows T to string here
console.log(value.toUpperCase());
} else if (typeof value === "number") {
// And to number here
console.log(value.toFixed(2));
}
}
TypeScript Compiler API Enhancements
Build custom tools with the TypeScript compiler:
import * as ts from "typescript";
// Create a program from config
const configPath = ts.findConfigFile(
"./",
ts.sys.fileExists,
"tsconfig.json"
);
const { config } = ts.readConfigFile(configPath!, ts.sys.readFile);
const { options, fileNames } = ts.parseJsonConfigFileContent(
config,
ts.sys,
"./"
);
const program = ts.createProgram(fileNames, options);
const checker = program.getTypeChecker();
// Analyze types in your codebase
program.getSourceFiles().forEach(sourceFile => {
if (!sourceFile.isDeclarationFile) {
ts.forEachChild(sourceFile, visit);
}
});
function visit(node: ts.Node) {
if (ts.isFunctionDeclaration(node) && node.name) {
const symbol = checker.getSymbolAtLocation(node.name);
console.log(`Found function: ${symbol?.getName()}`);
}
ts.forEachChild(node, visit);
}
Future TypeScript Features (Proposed)
Upcoming Features to Watch:
- Explicit Resource Management: Expand using/await using declarations
- Type-Level Arithmetic: Perform calculations at the type level
- Pattern Matching: Advanced destructuring with type narrowing
- Effect Types: Track side effects in the type system
- Nominal Types: First-class support for nominal typing
- Higher-Kinded Types: Abstract over type constructors
Best Practices for TypeScript 5.x
// 1. Use const type parameters for literal preservation
const routes = createRoutes([
{ path: "/users", method: "GET" },
{ path: "/posts", method: "POST" }
] as const);
// 2. Leverage satisfies for validation without widening
const config = {
api: { url: "https://api.com", timeout: 5000 },
features: { darkMode: true, analytics: false }
} satisfies AppConfig;
// 3. Use using declarations for resource management
async function processFile(path: string) {
await using file = await openFile(path);
// Automatic cleanup
}
// 4. Prefer type-only imports
import type { User, Product } from "./types";
import { fetchUser, fetchProduct } from "./api";
// 5. Use NoInfer to control type inference
function merge<T>(base: T, override: NoInfer<Partial<T>>): T {
return { ...base, ...override };
}
Migration Strategy to TypeScript 5.x
// 1. Update package.json
{
"devDependencies": {
"typescript": "^5.4.0"
}
}
// 2. Update tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"moduleResolution": "bundler",
// Disable experimental decorators if using standard ones
"experimentalDecorators": false
}
}
// 3. Test your build
npm install
npm run build
npm run type-check
// 4. Replace deprecated patterns
// Old: experimental decorators
// New: standard decorators (if using decorators)
// Old: type assertions everywhere
// New: satisfies operator
// Old: manual resource cleanup
// New: using declarations
TypeScript Playground and Debugging
Use modern debugging tools:
// Enable advanced diagnostics
{
"compilerOptions": {
"extendedDiagnostics": true,
"generateTrace": "./trace"
}
}
// Analyze trace with:
# npm install -g @typescript/analyze-trace
# analyze-trace ./trace
Real-World Example: Modern TypeScript Application
// config.ts
export const appConfig = {
api: {
baseUrl: "https://api.example.com",
timeout: 5000,
retries: 3
},
features: {
analytics: true,
darkMode: false
},
routes: {
home: "/",
users: "/users",
profile: "/users/:id"
}
} as const satisfies AppConfig;
// Type is preserved exactly
type ApiUrl = typeof appConfig.api.baseUrl;
// Type: "https://api.example.com"
// database.ts
class Database implements AsyncDisposable {
constructor(private config: DatabaseConfig) {}
async [Symbol.asyncDispose](): Promise<void> {
await this.disconnect();
}
async query<T>(sql: string): Promise<T[]> {
// Implementation
return [];
}
}
// Usage with automatic cleanup
async function getUsers() {
await using db = new Database(dbConfig);
return await db.query<User>("SELECT * FROM users");
// db automatically disconnected
}
// api.ts
function createApiClient<const Routes extends Record<string, Route>>(
baseUrl: string,
routes: Routes
) {
return {
request: <K extends keyof Routes>(
route: K,
...args: Routes[K] extends { params: infer P } ? [P] : []
) => {
// Implementation
}
};
}
const api = createApiClient("https://api.com", {
getUser: { path: "/users/:id", method: "GET", params: ["id"] },
createPost: { path: "/posts", method: "POST", params: [] }
} as const);
// Fully type-safe API calls
api.request("getUser", "user-123"); // ✓ OK
api.request("createPost"); // ✓ OK
// api.request("getUser"); // ✗ Error: missing params
Warning: Not all TypeScript 5.x features are supported in all runtimes. Check compatibility with your target environment (Node.js, browsers, Deno) before using advanced features like decorators and using declarations.
Community Resources
Stay Updated:
- Official Blog: devblogs.microsoft.com/typescript
- GitHub: github.com/microsoft/TypeScript
- Playground: typescriptlang.org/play
- Discord: TypeScript Community Discord
- Twitter: @typescript for announcements
- DefinitelyTyped: github.com/DefinitelyTyped/DefinitelyTyped
Final Project:
- Create a modern TypeScript 5.4+ application using const type parameters
- Implement resource management with using declarations
- Use satisfies operator for all configuration objects
- Create type-safe API client with exact literal types
- Implement decorators for logging and validation (if using standard decorators)
- Set up comprehensive type checking with strict mode
- Measure type coverage and aim for 95%+
- Document your type system architecture and patterns used
Congratulations! You've completed the TypeScript tutorial. You now have mastery of modern TypeScript including advanced types, generics, decorators, and the latest 5.x features. Continue building type-safe applications and stay updated with the TypeScript roadmap!