// Enum-like objectconst STATUS = { PENDING: 'pending', APPROVED: 'approved', REJECTED: 'rejected'} as const;type Status = typeof STATUS[keyof typeof STATUS];// Type: "pending" | "approved" | "rejected"function updateStatus(status: Status) { // status must be one of the literal values}updateStatus(STATUS.PENDING); // ✓ OKupdateStatus('pending'); // ✓ OKupdateStatus('invalid'); // ✗ Error// Configuration with constconst CONFIG = { api: { baseUrl: 'https://api.example.com', timeout: 5000, endpoints: { users: '/users', posts: '/posts' } }, features: { darkMode: true, notifications: false }} as const;// Extract typestype ApiEndpoint = typeof CONFIG.api.endpoints[keyof typeof CONFIG.api.endpoints];// Type: "/users" | "/posts"// Tuple with constconst point = [10, 20] as const;// Type: readonly [10, 20]function distance(p1: readonly [number, number], p2: readonly [number, number]) { return Math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2);}distance(point, [30, 40] as const); // ✓ OK
2. satisfies Operator for Type Checking TS 4.9
Feature
Syntax
Description
vs Annotation
satisfies
value satisfies Type
Validate type without widening inference
Preserves literal types
Type Annotation
value: Type
Widen type to annotation
Loses literal types
Use Case
Type validation + inference
Get both type safety and narrow types
Best of both worlds
Example: satisfies vs type annotation
type Colors = 'red' | 'green' | 'blue';// Problem with type annotation - loses literal typesconst palette1: Record<Colors, string | number[]> = { red: '#ff0000', green: [0, 255, 0], blue: '#0000ff'};palette1.red.toUpperCase(); // ✗ Error: string | number[] doesn't have toUpperCasepalette1.green.map(x => x * 2); // ✗ Error: string | number[] doesn't have map// Solution with satisfies - preserves literal typesconst palette2 = { red: '#ff0000', green: [0, 255, 0], blue: '#0000ff'} satisfies Record<Colors, string | number[]>;palette2.red.toUpperCase(); // ✓ OK: red is stringpalette2.green.map(x => x * 2); // ✓ OK: green is number[]palette2.yellow = '#ffff00'; // ✗ Error: yellow not in Colors// satisfies validates structure but keeps narrow typestype RGB = [number, number, number];const color = [255, 0, 0] satisfies RGB;// Type: [number, number, number] (tuple, not array)color[0] = 100; // ✓ OKcolor[3] = 50; // ✗ Error: tuple only has 3 elements
Example: Practical satisfies patterns
// Route configuration with validationtype Route = { path: string; component: string; children?: Route[];};const routes = [ { path: '/', component: 'Home' }, { path: '/about', component: 'About', children: [ { path: '/about/team', component: 'Team' } ] }] satisfies Route[];// Type is preserved as literal array, not Route[]const homePath = routes[0].path; // Type: string, not widenedroutes[0].path = '/home'; // ✓ Can mutate// API response schema validationtype ApiResponse<T> = { status: 'success' | 'error'; data?: T; error?: string;};const response = { status: 'success', data: { id: 1, name: 'Alice' }} satisfies ApiResponse<{ id: number; name: string }>;if (response.status === 'success') { // response.data is { id: number; name: string }, not optional console.log(response.data.name); // ✓ OK}// Configuration with mixed typestype Config = { [key: string]: string | number | boolean;};const config = { host: 'localhost', port: 3000, ssl: true, debug: false} satisfies Config;// Preserves specific typesconst port: number = config.port; // ✓ OK: port is numberconst host: string = config.host; // ✓ OK: host is string
Example: Complex satisfies scenarios
// Event handlers with satisfiestype EventHandlers = { [K in string]: (...args: any[]) => void;};const handlers = { onClick: (e: MouseEvent) => console.log(e.clientX), onSubmit: (data: FormData) => console.log(data), onError: (error: Error) => console.error(error.message)} satisfies EventHandlers;// Type of handlers.onClick is (e: MouseEvent) => void, not (...args: any[]) => voidhandlers.onClick(new MouseEvent('click')); // ✓ Type-safe// Discriminated unions with satisfiestype Action = | { type: 'INCREMENT'; by: number } | { type: 'DECREMENT'; by: number } | { type: 'RESET' };const actions = [ { type: 'INCREMENT', by: 1 }, { type: 'DECREMENT', by: 5 }, { type: 'RESET' }] satisfies Action[];// Type preserved: each action has its specific shapeactions[0].by; // ✓ OK: numberactions[2].by; // ✗ Error: RESET doesn't have 'by'
Note: Use satisfies when you want:
Type validation without widening to the annotation type
To preserve literal types while ensuring structural compatibility
Autocomplete for object properties while maintaining narrow types
Both compile-time checking and runtime-friendly types
3. using Declaration for Resource Management TS 5.2
Feature
Syntax
Description
Disposal
using Declaration
using resource = init();
Auto-dispose resource when scope exits
Calls Symbol.dispose
await using
await using resource = init();
Async disposal when scope exits
Calls Symbol.asyncDispose
Disposable
Symbol.dispose
Interface for sync resource cleanup
Automatic cleanup
AsyncDisposable
Symbol.asyncDispose
Interface for async resource cleanup
Async cleanup
Example: Basic using declaration
// Implement Disposable interfaceclass FileHandle implements Disposable { constructor(private path: string) { console.log(`Opening ${path}`); } read() { return `Content of ${this.path}`; } [Symbol.dispose]() { console.log(`Closing ${this.path}`); // Cleanup logic here }}// Traditional approach - manual cleanupfunction readFileOld() { const file = new FileHandle('data.txt'); try { return file.read(); } finally { file[Symbol.dispose](); }}// With using - automatic cleanupfunction readFileNew() { using file = new FileHandle('data.txt'); return file.read(); // file[Symbol.dispose]() called automatically at scope exit}// Multiple resourcesfunction processFiles() { using file1 = new FileHandle('input.txt'); using file2 = new FileHandle('output.txt'); // Use both files const content = file1.read(); // Both disposed in reverse order (LIFO) // file2 disposed first, then file1}
Example: Async disposal with await using
// Implement AsyncDisposableclass DatabaseConnection implements AsyncDisposable { constructor(private connectionString: string) { console.log('Connecting...'); } async query(sql: string) { // Execute query return []; } async [Symbol.asyncDispose]() { console.log('Disconnecting...'); // Async cleanup await this.closeConnection(); } private async closeConnection() { // Wait for pending operations await new Promise(resolve => setTimeout(resolve, 100)); }}// Async resource managementasync function queryDatabase() { await using db = new DatabaseConnection('mongodb://localhost'); const results = await db.query('SELECT * FROM users'); // db[Symbol.asyncDispose]() called automatically return results;}// Multiple async resourcesasync function processData() { await using db = new DatabaseConnection('db1'); await using cache = new CacheConnection('redis://localhost'); const data = await db.query('SELECT * FROM data'); await cache.set('key', data); // Disposed in reverse order: cache, then db}
Example: Practical resource management patterns
// Lock/Mutex patternclass Lock implements Disposable { private locked = false; acquire() { if (this.locked) throw new Error('Already locked'); this.locked = true; } [Symbol.dispose]() { this.locked = false; }}function criticalSection() { using lock = new Lock(); lock.acquire(); // Critical code here console.log('In critical section'); // Lock automatically released at scope exit}// Transaction patternclass Transaction implements AsyncDisposable { private committed = false; async execute(sql: string) { // Execute SQL } async commit() { this.committed = true; // Commit transaction } async [Symbol.asyncDispose]() { if (!this.committed) { console.log('Rolling back transaction'); // Rollback if not committed } }}async function transferMoney(from: string, to: string, amount: number) { await using tx = new Transaction(); await tx.execute(`UPDATE accounts SET balance = balance - ${amount} WHERE id = '${from}'`); await tx.execute(`UPDATE accounts SET balance = balance + ${amount} WHERE id = '${to}'`); await tx.commit(); // Transaction disposed: commit() called, so no rollback}// Cleanup multiple resourcesclass ResourcePool implements Disposable { private resources: Disposable[] = []; add(resource: Disposable) { this.resources.push(resource); } [Symbol.dispose]() { // Dispose all resources in reverse order for (let i = this.resources.length - 1; i >= 0; i--) { this.resources[i][Symbol.dispose](); } }}
Note: The using declaration implements explicit resource
management (similar to C#'s using, Python's with, Java's try-with-resources). Resources are disposed in
reverse declaration order (LIFO).
4. Decorator Metadata and Reflect API TS 5.0
Feature
Status
Description
Use Case
Decorator Metadata
Stage 3 Proposal
Attach metadata to decorated declarations
Reflection, DI frameworks
Symbol.metadata
New symbol
Access decorator metadata
Runtime reflection
context.metadata
Decorator context
Store metadata during decoration
Framework integration
Reflect Metadata API
Library (reflect-metadata)
Design-time type reflection
DI, validation
Example: Modern decorators with metadata (TS 5.0+)
// Enable in tsconfig.json:// "experimentalDecorators": false (use new decorators)// Class decorator with metadatafunction Entity(tableName: string) { return function<T extends { new(...args: any[]): {} }>( target: T, context: ClassDecoratorContext ) { // Store metadata context.metadata[Symbol.for('tableName')] = tableName; return class extends target { static tableName = tableName; }; };}// Method decorator with metadatafunction Route(path: string, method: string) { return function( target: Function, context: ClassMethodDecoratorContext ) { // Store route metadata if (!context.metadata.routes) { context.metadata.routes = []; } (context.metadata.routes as any[]).push({ path, method, handler: context.name }); };}@Entity('users')class User { @Route('/users', 'GET') getUsers() { return []; } @Route('/users/:id', 'GET') getUser(id: string) { return { id }; }}// Access metadataconst metadata = (User as any)[Symbol.metadata];console.log(metadata[Symbol.for('tableName')]); // 'users'console.log(metadata.routes); // Array of route configs
Example: Reflect Metadata API (legacy approach)
// npm install reflect-metadata// Enable in tsconfig.json:// "experimentalDecorators": true// "emitDecoratorMetadata": trueimport 'reflect-metadata';// Define metadata keysconst REQUIRED_KEY = Symbol('required');const VALIDATION_KEY = Symbol('validation');// Property decorator using Reflectfunction Required(target: any, propertyKey: string) { Reflect.defineMetadata(REQUIRED_KEY, true, target, propertyKey);}function MinLength(length: number) { return function(target: any, propertyKey: string) { Reflect.defineMetadata(VALIDATION_KEY, { minLength: length }, target, propertyKey); };}class CreateUserDto { @Required @MinLength(3) username!: string; @Required email!: string; age?: number;}// Validation using metadatafunction validate(obj: any): string[] { const errors: string[] = []; for (const key of Object.keys(obj)) { // Check required const isRequired = Reflect.getMetadata(REQUIRED_KEY, obj, key); if (isRequired && !obj[key]) { errors.push(`${key} is required`); } // Check validation rules const validation = Reflect.getMetadata(VALIDATION_KEY, obj, key); if (validation?.minLength && obj[key]?.length < validation.minLength) { errors.push(`${key} must be at least ${validation.minLength} characters`); } } return errors;}const dto = new CreateUserDto();dto.username = 'ab'; // Too shortconst errors = validate(dto); // ['email is required', 'username must be at least 3 characters']
Example: Design-time type reflection
import 'reflect-metadata';// Automatic type reflection with emitDecoratorMetadatafunction Log(target: any, propertyKey: string) { // Get design-time type information const type = Reflect.getMetadata('design:type', target, propertyKey); const paramTypes = Reflect.getMetadata('design:paramtypes', target, propertyKey); const returnType = Reflect.getMetadata('design:returntype', target, propertyKey); console.log(`${propertyKey} type:`, type?.name); console.log(`${propertyKey} params:`, paramTypes?.map((t: any) => t.name)); console.log(`${propertyKey} return:`, returnType?.name);}class Service { @Log processUser(user: User, id: number): Promise<boolean> { return Promise.resolve(true); }}// Logs: // processUser type: Function// processUser params: ['User', 'Number']// processUser return: Promise// Dependency injection with type reflectionconst INJECT_KEY = Symbol('inject');function Injectable() { return function<T extends { new(...args: any[]): {} }>(target: T) { // Store parameter types for DI const paramTypes = Reflect.getMetadata('design:paramtypes', target) || []; Reflect.defineMetadata(INJECT_KEY, paramTypes, target); return target; };}@Injectable()class UserService { constructor(private db: DatabaseService, private logger: LoggerService) {}}// DI Containerclass Container { resolve<T>(target: new (...args: any[]) => T): T { const params = Reflect.getMetadata(INJECT_KEY, target) || []; const instances = params.map((param: any) => this.resolve(param)); return new target(...instances); }}const container = new Container();const userService = container.resolve(UserService);
5. Import Attributes and JSON Modules TS 5.3
Feature
Syntax
Description
Use Case
Import Attributes
import x from 'mod' with { }
Specify module type/attributes on import
JSON, CSS modules
JSON Modules
import json from './data.json' with { type: 'json' }
Import JSON as module
Static JSON import
resolveJsonModule
tsconfig option
Enable JSON module resolution
Type-safe JSON imports
Import Assertions
assert { type: 'json' }
Legacy syntax (pre-TS 5.3)
Deprecated
Example: JSON module imports
// tsconfig.json{ "compilerOptions": { "module": "esnext", "resolveJsonModule": true, "esModuleInterop": true }}// data.json{ "name": "MyApp", "version": "1.0.0", "config": { "apiUrl": "https://api.example.com", "timeout": 5000 }}// Modern syntax (TS 5.3+) with import attributesimport data from './data.json' with { type: 'json' };console.log(data.name); // ✓ Type-safe: stringconsole.log(data.config.timeout); // ✓ Type-safe: number// Type is inferred from JSON structuretype DataType = typeof data;// {// name: string;// version: string;// config: { apiUrl: string; timeout: number; }// }// Legacy syntax (pre-TS 5.3) - still worksimport legacyData from './data.json' assert { type: 'json' };// Without attributes (requires resolveJsonModule)import simpleData from './data.json';// Dynamic import with attributesconst dynamicData = await import('./data.json', { with: { type: 'json' }});
Example: CSS and other module types
// CSS Modules with import attributesimport styles from './styles.css' with { type: 'css' };// In browsers supporting CSS modulesdocument.adoptedStyleSheets = [styles];// TypeScript declaration for CSS modulesdeclare module '*.css' { const stylesheet: CSSStyleSheet; export default stylesheet;}// Web Assembly modulesimport wasmModule from './module.wasm' with { type: 'webassembly' };// Custom module types (requires bundler support)import config from './config.toml' with { type: 'toml' };import data from './data.yaml' with { type: 'yaml' };// Type declarations for custom formatsdeclare module '*.toml' { const content: Record<string, any>; export default content;}declare module '*.yaml' { const content: Record<string, any>; export default content;}
Example: Typed JSON imports
// config.json with strict typing{ "database": { "host": "localhost", "port": 5432, "name": "mydb" }, "features": { "auth": true, "logging": true }}// Import with type assertionimport configData from './config.json' with { type: 'json' };// Define expected structureinterface Config { database: { host: string; port: number; name: string; }; features: { auth: boolean; logging: boolean; };}// Validate at runtime (optional)function isValidConfig(data: unknown): data is Config { return ( typeof data === 'object' && data !== null && 'database' in data && 'features' in data );}// Use config with type safetyconst config: Config = configData;console.log(config.database.port); // ✓ Type: number// Array of dataimport users from './users.json' with { type: 'json' };interface User { id: number; name: string; email: string;}const typedUsers: User[] = users;typedUsers.forEach(user => { console.log(user.name.toUpperCase()); // ✓ Type-safe});
6. Node16/NodeNext Module Resolution
Feature
Description
Requirement
Behavior
Node16/NodeNext
Modern Node.js ESM resolution
File extensions required
Matches Node.js behavior
package.json "type"
Specify module system
"module" or "commonjs"
Determines .js meaning
Conditional Exports
Export different entry points
"exports" field
import vs require
File Extensions
Must include in imports
.js for .ts files
Mirrors runtime behavior
Example: Node16/NodeNext configuration
// package.json for ESM project{ "name": "my-package", "version": "1.0.0", "type": "module", // All .js files are ESM "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" }, "./utils": { "types": "./dist/utils.d.ts", "import": "./dist/utils.js" } }}// tsconfig.json{ "compilerOptions": { "target": "ES2022", "module": "Node16", // or "NodeNext" "moduleResolution": "Node16", // or "NodeNext" "outDir": "./dist", "declaration": true }}// src/utils.tsexport function formatDate(date: Date): string { return date.toISOString();}// src/index.ts - MUST use .js extensionimport { formatDate } from './utils.js'; // ✓ .js even though source is .tsimport { formatDate } from './utils'; // ✗ Error: must include extension// For directories, use indeximport * as utils from './utils/index.js'; // ✓ OK// Relative imports need extensionsimport type { User } from './types.js'; // ✓ OKimport type { User } from './types'; // ✗ Error
Example: Dual package (CJS + ESM)
// package.json for dual package{ "name": "dual-package", "version": "1.0.0", "type": "module", "main": "./dist/index.cjs", // CJS entry point "module": "./dist/index.js", // ESM entry point "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", // ESM "require": "./dist/index.cjs", // CJS "default": "./dist/index.js" } }, "files": ["dist"]}// Build with both formats// tsconfig.json for ESM build{ "compilerOptions": { "module": "Node16", "outDir": "./dist" }}// Build CJS separately or use bundler// Output:// dist/index.js (ESM)// dist/index.cjs (CJS)// dist/index.d.ts (types)// Consumers can use either// ESM consumer:import pkg from 'dual-package';// CJS consumer:const pkg = require('dual-package');
Example: Subpath exports and patterns
// package.json with detailed exports{ "name": "my-lib", "type": "module", "exports": { // Main entry ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" }, // Specific subpaths "./utils": "./dist/utils.js", "./helpers": "./dist/helpers.js", // Pattern exports (all files in utils/) "./utils/*": { "types": "./dist/utils/*.d.ts", "import": "./dist/utils/*.js" }, // Conditional exports "./server": { "node": "./dist/server.js", "default": "./dist/server-browser.js" }, // Block access to internal files "./internal/*": null, // Package.json access "./package.json": "./package.json" }}// Usage:import lib from 'my-lib'; // Main entryimport { util } from 'my-lib/utils'; // Subpathimport { helper } from 'my-lib/utils/string.js'; // Patternimport server from 'my-lib/server'; // Conditionalimport internal from 'my-lib/internal/secret'; // ✗ Error: blocked
Note: Node16/NodeNext resolution is the modern standard for
Node.js projects. Key differences from classic Node resolution:
File extensions are required in imports
Use .js extension even when importing .ts files
package.json "type" field determines module system
Respects "exports" field for subpath exports
No automatic index.js resolution without explicit path
Warning: Node16/NodeNext is stricter than classic resolution:
Path mappings (tsconfig paths) are not supported
Must use .js extension in imports, not .ts
Directory imports require /index.js explicitly
Mixing ESM and CJS requires careful package.json configuration
TypeScript 5+ Features Summary
const type parameters - Preserve literal types in generic functions with
<const T>
satisfies operator - Type validation without widening, preserves literal
types
using declaration - Automatic resource disposal with Symbol.dispose
Decorator metadata - Attach metadata to decorators for reflection and DI
Import attributes - Specify module type with 'with { type }' syntax
Node16/NodeNext - Modern ESM resolution matching Node.js behavior