TypeScript

TypeScript 5.x Features & Future

42 min Lesson 30 of 40

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:
  1. Create a modern TypeScript 5.4+ application using const type parameters
  2. Implement resource management with using declarations
  3. Use satisfies operator for all configuration objects
  4. Create type-safe API client with exact literal types
  5. Implement decorators for logging and validation (if using standard decorators)
  6. Set up comprehensive type checking with strict mode
  7. Measure type coverage and aim for 95%+
  8. 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!