TypeScript 4 5

Release Overview

TypeScript 4.5 was released on November 17, 2021, introducing the Awaited utility type for better Promise handling, template string types as discriminants, ECMAScript import assertions, and private field presence checks with the in operator.

Key Metrics:

  • Release Date: November 17, 2021
  • Major Focus: Async/await type handling, advanced type narrowing, modern module features
  • Breaking Changes: Minimal
  • Performance: Improved Promise type inference

The Awaited<Type> Utility Type

Landmark Feature: New built-in utility type that recursively unwraps Promise types.

The Problem Before TypeScript 4.5

Before: No standard way to extract the resolved type from nested Promises.

// TypeScript 4.4 and earlier
type Response = Promise<Promise<string>>;
// => Nested Promise type

// No clean way to get the final string type
type Result = Response extends Promise<infer T> ? T : never;
// => Result is Promise<string> (still wrapped!)

type FinalResult = Result extends Promise<infer U> ? U : never;
// => FinalResult is string (manual unwrapping required)

With Awaited<Type>

Solution: Built-in utility type recursively unwraps all Promise layers.

// TypeScript 4.5 and later
type Response = Promise<Promise<string>>;
// => Nested Promise type

type Result = Awaited<Response>;
// => ✅ Result is string (fully unwrapped)

type DeepNested = Promise<Promise<Promise<number>>>;
// => Triple-nested Promise

type Unwrapped = Awaited<DeepNested>;
// => ✅ Unwrapped is number (all layers removed)

type MixedType = string | Promise<number>;
// => Union with Promise

type UnwrappedMixed = Awaited<MixedType>;
// => ✅ UnwrappedMixed is string | number

Real-World Application: Async Function Return Types

Infer return types from async functions:

async function fetchUser(id: number) {
  const response = await fetch(`/api/users/${id}`);
  // => response is Response

  const data = await response.json();
  // => data is any

  return {
    id: data.id as number,
    name: data.name as string,
    email: data.email as string,
  };
  // => Returns Promise<{ id: number; name: string; email: string }>
}

// Extract the resolved type
type User = Awaited<ReturnType<typeof fetchUser>>;
// => ✅ User is { id: number; name: string; email: string }
// => No more Promise<...> wrapping

// Use in other functions
function displayUser(user: User) {
  console.log(`${user.name} (${user.email})`);
  // => user is plain object, not Promise
}

// Works with nested async calls
async function getAndDisplayUser(id: number) {
  const user = await fetchUser(id);
  // => user is User (inferred correctly)

  displayUser(user);
  // => ✅ Type-safe
}

Real-World Application: API Response Handling

Type-safe async data transformation:

async function fetchPosts() {
  const response = await fetch("/api/posts");
  // => response is Response

  return response.json();
  // => Returns Promise<any>
}

async function transformPosts() {
  const posts = await fetchPosts();
  // => posts is any

  return posts.map((post: any) => ({
    id: post.id,
    title: post.title,
    excerpt: post.content.substring(0, 100),
  }));
  // => Returns Promise<Array<{ id: any; title: any; excerpt: string }>>
}

// Extract transformed post type
type TransformedPost = Awaited<ReturnType<typeof transformPosts>>[number];
// => ✅ TransformedPost is { id: any; title: any; excerpt: string }

// Type-safe rendering
function renderPost(post: TransformedPost) {
  return `
    <article>
      <h2>${post.title}</h2>
      <p>${post.excerpt}</p>
    </article>
  `;
  // => All properties type-checked
}

Real-World Application: Parallel Promise Handling

Type inference for Promise.all and Promise.race:

async function loadUserData(userId: number) {
  const [profile, posts, comments] = await Promise.all([
    fetch(`/api/users/${userId}`).then((r) => r.json()),
    // => Returns Promise<any>

    fetch(`/api/users/${userId}/posts`).then((r) => r.json()),
    // => Returns Promise<any>

    fetch(`/api/users/${userId}/comments`).then((r) => r.json()),
    // => Returns Promise<any>
  ]);
  // => profile, posts, comments all any

  return { profile, posts, comments };
  // => Returns Promise<{ profile: any; posts: any; comments: any }>
}

// Extract result type
type UserData = Awaited<ReturnType<typeof loadUserData>>;
// => ✅ UserData is { profile: any; posts: any; comments: any }

// Works with Promise.race
async function fetchWithFallback() {
  return Promise.race([
    fetch("/api/primary").then((r) => r.json()),
    // => Returns Promise<any>

    fetch("/api/fallback").then((r) => r.json()),
    // => Returns Promise<any>
  ]);
  // => Returns Promise<any>
}

type FallbackData = Awaited<ReturnType<typeof fetchWithFallback>>;
// => ✅ FallbackData is any (from first resolved Promise)

Real-World Application: Database Query Results

Type-safe ORM query results:

class Database {
  async query<T>(sql: string): Promise<T[]> {
    // Execute SQL query
    return [] as T[];
    // => Returns Promise<T[]>
  }
}

const db = new Database();
// => db is Database

async function getUserPosts(userId: number) {
  const posts = await db.query<{ id: number; title: string; content: string }>(
    `SELECT * FROM posts WHERE user_id = ${userId}`,
  );
  // => posts is Array<{ id: number; title: string; content: string }>

  return posts;
  // => Returns Promise<Array<{ id: number; title: string; content: string }>>
}

// Extract post type
type Post = Awaited<ReturnType<typeof getUserPosts>>[number];
// => ✅ Post is { id: number; title: string; content: string }

// Type-safe post processing
function formatPost(post: Post): string {
  return `${post.title}: ${post.content.substring(0, 50)}...`;
  // => All properties available and type-checked
}

Template String Types as Discriminants

Feature: Template literal types can now be used to narrow discriminated unions.

Example with Route Handling

type Route =
  | { path: `/${string}`; method: "GET"; handler: () => void }
  | { path: `/api/${string}`; method: "POST"; handler: (data: unknown) => void }
  | { path: `/admin/${string}`; method: "DELETE"; handler: (id: number) => void };

function handleRoute(route: Route) {
  if (route.path.startsWith("/api/")) {
    // => ✅ TypeScript narrows to second union member
    console.log(route.method);
    // => method is "POST"

    route.handler({ data: "example" });
    // => handler is (data: unknown) => void
  }

  if (route.path.startsWith("/admin/")) {
    // => ✅ TypeScript narrows to third union member
    console.log(route.method);
    // => method is "DELETE"

    route.handler(123);
    // => handler is (id: number) => void
  }
}

Real-World Application: Event System

Type-safe event handling with template string discriminants:

type Event =
  | { type: `user:${string}`; userId: number; timestamp: Date }
  | { type: `post:${string}`; postId: number; timestamp: Date }
  | { type: `comment:${string}`; commentId: number; userId: number; timestamp: Date };

function logEvent(event: Event) {
  const now = new Date();
  // => now is Date

  if (event.type.startsWith("user:")) {
    // => ✅ Narrowed to first union member
    console.log(`User event: ${event.userId} at ${event.timestamp}`);
    // => userId is number
    // => timestamp is Date
  }

  if (event.type.startsWith("post:")) {
    // => ✅ Narrowed to second union member
    console.log(`Post event: ${event.postId} at ${event.timestamp}`);
    // => postId is number
  }

  if (event.type.startsWith("comment:")) {
    // => ✅ Narrowed to third union member
    console.log(`Comment ${event.commentId} by user ${event.userId}`);
    // => commentId is number
    // => userId is number
  }
}

// Example usage
const userEvent: Event = {
  type: "user:login",
  userId: 123,
  timestamp: new Date(),
};
// => ✅ Valid user event

logEvent(userEvent);
// => Correctly narrows type

Type-Only Import/Export Syntax Improvements

Feature: type modifier can be used on individual named imports.

Syntax

// Before TypeScript 4.5 - separate import statements
import type { User } from "./types";
// => Type-only import (erased at runtime)

import { fetchUser } from "./api";
// => Value import (kept at runtime)

// TypeScript 4.5 - mixed imports
import { fetchUser, type User, type Post } from "./api";
// => ✅ fetchUser is value import
// => ✅ User and Post are type-only imports
// => Cleaner single import statement

Real-World Application: API Client

Organize type and value imports:

// api.ts
export interface User {
  id: number;
  name: string;
}

export interface Post {
  id: number;
  title: string;
}

export async function fetchUser(id: number): Promise<User> {
  // Implementation
  return { id, name: "Alice" };
}

export async function fetchPosts(): Promise<Post[]> {
  // Implementation
  return [];
}

// consumer.ts - TypeScript 4.5 syntax
import { fetchUser, fetchPosts, type User, type Post } from "./api";
// => ✅ Functions are runtime imports
// => ✅ Interfaces are type-only imports (erased)

async function displayUserWithPosts(id: number): Promise<void> {
  const user: User = await fetchUser(id);
  // => user is User

  const posts: Post[] = await fetchPosts();
  // => posts is Post[]

  console.log(`User ${user.name} has ${posts.length} posts`);
}

Import Assertions for JSON Modules

Feature: Support for ECMAScript import assertions, enabling type-safe JSON imports.

Syntax

// Import JSON with assertion
import config from "./config.json" assert { type: "json" };
// => config is inferred from JSON structure

// TypeScript infers type from JSON content
const apiUrl = config.apiUrl;
// => apiUrl is string (if defined in JSON)

const port = config.port;
// => port is number (if defined in JSON)

Real-World Application: Configuration Management

Type-safe configuration loading:

// config.json
{
  "apiUrl": "https://api.example.com",
  "port": 3000,
  "features": {
    "darkMode": true,
    "analytics": false
  }
}

// app.ts
import appConfig from "./config.json" assert { type: "json" };
// => appConfig is typed from JSON structure

function initializeApp() {
  const server = createServer(appConfig.port);
  // => appConfig.port is number

  const api = new APIClient(appConfig.apiUrl);
  // => appConfig.apiUrl is string

  if (appConfig.features.darkMode) {
    // => appConfig.features.darkMode is boolean
    enableDarkMode();
  }
}

Private Field Presence Checks with in Operator

Feature: The in operator now works with private fields for existence checks.

Syntax

class Person {
  #name: string;
  // => Private field

  constructor(name: string) {
    this.#name = name;
    // => Initialize private field
  }

  equals(other: unknown): boolean {
    if (!(other instanceof Person)) {
      return false;
      // => Type guard: other must be Person
    }

    // TypeScript 4.5 - check private field presence
    if (#name in other) {
      // => ✅ Checks if other has #name private field
      return this.#name === other.#name;
      // => Safe comparison
    }

    return false;
  }
}

Real-World Application: Secure Object Comparison

Type-safe private field checks:

class SecureToken {
  #secret: string;
  // => Private secret

  #expiresAt: Date;
  // => Private expiration

  constructor(secret: string, expiresAt: Date) {
    this.#secret = secret;
    this.#expiresAt = expiresAt;
  }

  isValid(other: unknown): boolean {
    // Check if other is SecureToken instance
    if (!(other instanceof SecureToken)) {
      return false;
      // => Not a SecureToken
    }

    // Check private field presence
    if (#secret in other && #expiresAt in other) {
      // => ✅ other has both private fields
      const isSecretMatch = this.#secret === other.#secret;
      // => Compare secrets

      const isNotExpired = other.#expiresAt > new Date();
      // => Check expiration

      return isSecretMatch && isNotExpired;
    }

    return false;
  }
}

--module es2022 Support

Feature: New module target for ECMAScript 2022 features.

Configuration

{
  "compilerOptions": {
    "module": "es2022",
    "target": "es2022"
  }
}

Enables:

  • Top-level await
  • Import assertions
  • Modern module resolution

Performance Improvements

Build Performance:

  • 10-15% faster Promise type unwrapping with Awaited
  • Improved template literal type checking
  • Better incremental compilation for large projects

Editor Performance:

  • Faster IntelliSense for async functions
  • Reduced lag with complex template literal types
  • Better responsiveness with large union types

Breaking Changes

Minimal breaking changes:

  1. lib.d.ts updates - ES2022 features may conflict with existing definitions
  2. Template string type narrowing - May expose previously hidden type errors
  3. Private field checks - New runtime behavior with in operator

Migration Guide

Step 1: Update TypeScript

npm install -D typescript@4.5
# => Installs TypeScript 4.5

Step 2: Leverage Awaited<Type>

Replace manual Promise unwrapping:

// Before
type Result = ReturnType<typeof asyncFunc> extends Promise<infer T> ? T : never;

// After - use Awaited
type Result = Awaited<ReturnType<typeof asyncFunc>>;
// => ✅ Cleaner and handles nested Promises

Step 3: Use Inline Type Imports

Simplify import statements:

// Before
import type { User, Post } from "./types";
import { fetchData } from "./api";

// After - combine imports
import { fetchData, type User, type Post } from "./api";
// => ✅ Single import statement

Step 4: Enable Import Assertions

For Node.js 17+ projects, use import assertions:

import config from "./config.json" assert { type: "json" };
// => Type-safe JSON imports

Step 5: Consider --module es2022

For modern environments, update tsconfig.json:

{
  "compilerOptions": {
    "module": "es2022",
    "target": "es2022"
  }
}

Upgrade Recommendations

Immediate Actions:

  1. Update to TypeScript 4.5 for Awaited type
  2. Refactor async function type extraction with Awaited
  3. Use inline type imports for cleaner code organization

Future Considerations:

  1. Migrate to es2022 module target for modern projects
  2. Leverage template string discriminants for event systems
  3. Use import assertions for type-safe JSON configuration

Summary

TypeScript 4.5 (November 2021) enhanced async/await and modern module support:

  • Awaited<Type> utility - Recursively unwrap Promise types
  • Template string discriminants - Type narrowing with template literal types
  • Inline type imports - Mixed type/value imports in single statement
  • Import assertions - Type-safe JSON module imports
  • Private field presence checks - in operator works with private fields
  • --module es2022 - Support for latest ECMAScript module features

Impact: Awaited type became essential for async TypeScript development, eliminating boilerplate for Promise type unwrapping.

Next Steps:

References

Last updated