TypeScript 5 0

Release Overview

TypeScript 5.0 was released on March 16, 2023, marking a major milestone in TypeScript’s evolution with ECMAScript standard decorators, const type parameters, and significant performance improvements.

Key Metrics:

  • Release Date: March 16, 2023
  • Major Focus: Standard decorators, const type parameters, enum improvements
  • Breaking Changes: Decorator changes, module resolution updates
  • Performance: 10-15% faster build times, 20-40% smaller package size

Decorators - ECMAScript Standard Support

Landmark Feature: Full support for Stage 3 ECMAScript decorators proposal, replacing experimental decorators.

The Evolution of Decorators

Before TypeScript 5.0: Experimental decorators (non-standard, required experimentalDecorators)

TypeScript 5.0: Standard decorators aligned with ECMAScript proposal

// Standard decorators (TypeScript 5.0+)
function logged(value: any, context: ClassMethodDecoratorContext) {
  // context provides metadata about decorated element
  const methodName = String(context.name);

  return function (this: any, ...args: any[]) {
    console.log(`Calling ${methodName} with:`, args);
    const result = value.call(this, ...args);
    console.log(`${methodName} returned:`, result);
    return result;
  };
}

class Calculator {
  @logged
  add(a: number, b: number): number {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(2, 3);
// Output:
// Calling add with: [2, 3]
// add returned: 5

Real-World Application: Validation Decorator

Type-safe validation using standard decorators:

// Validation decorator with metadata
function validate(validationFn: (value: any) => boolean, message: string) {
  return function (target: any, context: ClassFieldDecoratorContext) {
    return function (this: any, initialValue: any) {
      return {
        get() {
          return initialValue;
        },
        set(newValue: any) {
          if (!validationFn(newValue)) {
            throw new Error(`${String(context.name)}: ${message}`);
          }
          initialValue = newValue;
        },
      };
    };
  };
}

class User {
  @validate((val) => val.length >= 3, "Username must be at least 3 characters")
  username: string;

  @validate((val) => val.includes("@"), "Invalid email format")
  email: string;

  constructor(username: string, email: string) {
    this.username = username;
    this.email = email;
  }
}

// ✅ Valid
const user1 = new User("john", "john@example.com");

// ❌ Throws: Username must be at least 3 characters
const user2 = new User("jo", "jo@example.com");

// ❌ Throws: Invalid email format
const user3 = new User("jane", "jane-example.com");

Real-World Application: Memoization Decorator

Cache expensive function results:

function memoize(target: any, context: ClassMethodDecoratorContext) {
  const cache = new Map<string, any>();

  return function (this: any, ...args: any[]) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      console.log(`Cache hit for ${String(context.name)}(${key})`);
      return cache.get(key);
    }

    console.log(`Cache miss for ${String(context.name)}(${key})`);
    const result = target.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

class Fibonacci {
  @memoize
  calculate(n: number): number {
    if (n <= 1) return n;
    return this.calculate(n - 1) + this.calculate(n - 2);
  }
}

const fib = new Fibonacci();
console.log(fib.calculate(10)); // Cache misses during recursion
console.log(fib.calculate(10)); // Cache hit - instant result

Real-World Application: Dependency Injection

Register and inject services automatically:

// Service registry
const serviceRegistry = new Map<any, any>();

// Register decorator
function injectable(target: any, context: ClassDecoratorContext) {
  serviceRegistry.set(target, new target());
}

// Inject decorator
function inject(serviceClass: any) {
  return function (target: any, context: ClassFieldDecoratorContext) {
    return function (this: any) {
      return serviceRegistry.get(serviceClass);
    };
  };
}

// Define services
@injectable
class LoggerService {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

@injectable
class DatabaseService {
  query(sql: string) {
    console.log(`[DB] Executing: ${sql}`);
    return [{ id: 1, name: "User 1" }];
  }
}

// Use dependency injection
class UserController {
  @inject(LoggerService)
  logger!: LoggerService;

  @inject(DatabaseService)
  db!: DatabaseService;

  getUsers() {
    this.logger.log("Fetching users");
    return this.db.query("SELECT * FROM users");
  }
}

const controller = new UserController();
controller.getUsers();
// Output:
// [LOG] Fetching users
// [DB] Executing: SELECT * FROM users

Const Type Parameters

Feature: Type parameters that prevent type widening and preserve literal types.

The Problem with Type Widening

Before const type parameters:

// Generic function without const
function makeArray<T>(value: T): T[] {
  return [value];
}

const numbers = makeArray(1); // Type: number[]
// Lost literal type 1

const strings = makeArray("hello"); // Type: string[]
// Lost literal type "hello"

// Can't create precise union types
type AllowedValues = (typeof numbers)[number]; // Type: number (too wide)

With Const Type Parameters

Solution: Preserve exact literal types using const modifier.

// Generic function WITH const
function makeArray<const T>(value: T): T[] {
  return [value];
}

const numbers = makeArray(1); // Type: 1[]
// ✅ Preserves literal type 1

const strings = makeArray("hello"); // Type: "hello"[]
// ✅ Preserves literal type "hello"

// Precise union types
type AllowedValues = (typeof numbers)[number]; // Type: 1 (precise!)

Real-World Application: Type-Safe Configuration

Create configurations with exact literal types:

function createConfig<const T extends Record<string, any>>(config: T): T {
  return config;
}

const apiConfig = createConfig({
  baseUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3,
  endpoints: {
    users: "/api/users",
    posts: "/api/posts",
  },
});

// ✅ Type inference preserves exact values
type BaseUrl = typeof apiConfig.baseUrl;
// Type: "https://api.example.com" (exact string)

type Timeout = typeof apiConfig.timeout;
// Type: 5000 (exact number)

type Endpoints = keyof typeof apiConfig.endpoints;
// Type: "users" | "posts" (exact keys)

// Type-safe endpoint access
function callEndpoint(endpoint: Endpoints) {
  const url = apiConfig.baseUrl + apiConfig.endpoints[endpoint];
  // endpoint is precisely typed, no typos possible
}

callEndpoint("users"); // ✅ OK
callEndpoint("invalid"); // ❌ Error - not in Endpoints type

Real-World Application: Route Builder

Build type-safe route definitions:

function defineRoutes<const T extends Record<string, string>>(routes: T): T {
  return routes;
}

const routes = defineRoutes({
  home: "/",
  about: "/about",
  userProfile: "/user/:id",
  postDetail: "/post/:slug",
  adminDashboard: "/admin/dashboard",
});

// ✅ Exact literal types preserved
type RoutePath = (typeof routes)[keyof typeof routes];
// Type: "/" | "/about" | "/user/:id" | "/post/:slug" | "/admin/dashboard"

type RouteKey = keyof typeof routes;
// Type: "home" | "about" | "userProfile" | "postDetail" | "adminDashboard"

// Type-safe navigation
function navigate(route: RouteKey, params?: Record<string, string>) {
  let path = routes[route];
  if (params) {
    Object.entries(params).forEach(([key, value]) => {
      path = path.replace(`:${key}`, value);
    });
  }
  console.log(`Navigating to: ${path}`);
}

navigate("home"); // ✅ OK
navigate("userProfile", { id: "123" }); // ✅ OK
navigate("invalid"); // ❌ Error - not a valid route

Enum Enhancements

Feature: Enums can now have computed members in more scenarios and better support union types.

Enum with Computed Members

// Allowed in TypeScript 5.0
enum Permission {
  Read = 1 << 0, // 1
  Write = 1 << 1, // 2
  Delete = 1 << 2, // 4
  // Computed from other members
  ReadWrite = Read | Write, // 3
  FullAccess = Read | Write | Delete, // 7
}

function checkPermission(userPerm: Permission, required: Permission): boolean {
  return (userPerm & required) === required;
}

const userPermissions = Permission.ReadWrite;
console.log(checkPermission(userPermissions, Permission.Read)); // true
console.log(checkPermission(userPermissions, Permission.Write)); // true
console.log(checkPermission(userPermissions, Permission.Delete)); // false

Real-World Application: Feature Flags

Bitwise enum operations for efficient feature checks:

enum Features {
  None = 0,
  DarkMode = 1 << 0, // 1
  Notifications = 1 << 1, // 2
  Analytics = 1 << 2, // 4
  BetaFeatures = 1 << 3, // 8
  // Combinations
  DefaultUser = DarkMode | Notifications, // 3
  Premium = DefaultUser | Analytics | BetaFeatures, // 15
}

class FeatureManager {
  private enabledFeatures: Features;

  constructor(features: Features = Features.None) {
    this.enabledFeatures = features;
  }

  enable(feature: Features) {
    this.enabledFeatures |= feature;
  }

  disable(feature: Features) {
    this.enabledFeatures &= ~feature;
  }

  isEnabled(feature: Features): boolean {
    return (this.enabledFeatures & feature) === feature;
  }

  toggle(feature: Features) {
    this.enabledFeatures ^= feature;
  }
}

// Usage
const manager = new FeatureManager(Features.DefaultUser);
console.log(manager.isEnabled(Features.DarkMode)); // true
console.log(manager.isEnabled(Features.Analytics)); // false

manager.enable(Features.Analytics);
console.log(manager.isEnabled(Features.Analytics)); // true

manager.toggle(Features.DarkMode);
console.log(manager.isEnabled(Features.DarkMode)); // false

Supporting Types for Multiple Configuration Files

Feature: --verbatimModuleSyntax flag for predictable module output.

The Module Syntax Problem

Before TypeScript 5.0: Module output could be unpredictable based on content.

// Input TypeScript
import { foo } from "./module";
import type { Bar } from "./module";

// Output could vary based on tsconfig and usage

With --verbatimModuleSyntax

Solution: Emit exactly what you write, preserving import/export syntax.

// tsconfig.json
{
  "compilerOptions": {
    "verbatimModuleSyntax": true
  }
}

// Input TypeScript
import { foo } from "./module";      // ✅ Emitted as-is
import type { Bar } from "./module"; // ✅ Removed (type-only)

// Explicit type-only imports are ALWAYS removed
// Value imports are ALWAYS preserved
// No guessing or implicit behavior

Real-World Application: Library Authoring

Predictable output for library consumers:

// library.ts
export type Config = {
  apiKey: string;
  timeout: number;
};

export class Client {
  constructor(config: Config) {
    // Implementation
  }
}

// consumer.ts with verbatimModuleSyntax
import type { Config } from "./library"; // Not emitted (type-only)
import { Client } from "./library"; // Always emitted (value)

const config: Config = {
  apiKey: "abc123",
  timeout: 5000,
};

const client = new Client(config);

// Output JavaScript is predictable:
// import { Client } from "./library";
// (Config import completely removed)

Performance Improvements

Build Performance:

  • 10-15% faster type checking
  • 20-40% smaller npm package size
  • Faster --watch mode responsiveness
  • Reduced memory consumption

Editor Performance:

  • Faster IntelliSense for large projects
  • Improved responsiveness with decorators
  • Better performance with const type parameters

Breaking Changes

1. Decorator Changes

Old experimental decorators are incompatible with standard decorators:

// ❌ Experimental decorators (old)
// tsconfig: "experimentalDecorators": true

// ✅ Standard decorators (new)
// tsconfig: Remove "experimentalDecorators" or use both

Migration: Keep experimentalDecorators: true temporarily if using legacy decorators, migrate incrementally.

2. --verbatimModuleSyntax Implications

Stricter about import/export syntax:

// ❌ Error with verbatimModuleSyntax
import { SomeType } from "./module"; // SomeType is a type, not a value

// ✅ Must be explicit
import type { SomeType } from "./module";

3. Enum Behavior Changes

Computed enum members have stricter rules about what can be computed.

4. Module Resolution Updates

--module node16 and --module nodenext are now more strictly enforced.

Migration Guide

Step 1: Update TypeScript

npm install -D typescript@5.0

Step 2: Evaluate Decorators

If using experimental decorators:

// tsconfig.json - Keep experimental for now
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Step 3: Try Standard Decorators

Create new decorators using standard syntax for new code:

// New standard decorator
function logged(target: any, context: ClassMethodDecoratorContext) {
  // Standard decorator implementation
}

Step 4: Consider verbatimModuleSyntax

Enable for predictable module output (gradual adoption):

{
  "compilerOptions": {
    "verbatimModuleSyntax": true
  }
}

Step 5: Use Const Type Parameters

Replace type widening with const parameters:

// Before
function create<T>(value: T) { ... }

// After - preserve literals
function create<const T>(value: T) { ... }

Summary

TypeScript 5.0 (March 2023) marked a major milestone with standard decorators:

  • Standard decorators - ECMAScript Stage 3 proposal support
  • Const type parameters - Prevent type widening, preserve literals
  • Enum enhancements - Better computed members support
  • --verbatimModuleSyntax - Predictable module output
  • Performance gains - 10-15% faster builds, 20-40% smaller package

Impact: Standard decorators align TypeScript with JavaScript’s future, while const type parameters solve long-standing type precision issues.

Next Steps:

  • Continue to TypeScript 5.1 for easier implicit returns and JSX improvements
  • Return to Overview for full timeline

References

Last updated