English

A comprehensive guide to the TypeScript 'infer' keyword, explaining how to use it with conditional types for powerful type extraction and manipulation, including advanced use cases.

Mastering TypeScript Infer: Conditional Type Extraction for Advanced Type Manipulation

TypeScript's type system is incredibly powerful, allowing developers to create robust and maintainable applications. One of the key features enabling this power is the infer keyword used in conjunction with conditional types. This combination provides a mechanism for extracting specific types from complex type structures. This blog post delves deep into the infer keyword, explaining its functionality and showcasing advanced use cases. We'll explore practical examples applicable to diverse software development scenarios, from API interaction to complex data structure manipulation.

What are Conditional Types?

Before we dive into infer, let's quickly review conditional types. Conditional types in TypeScript allow you to define a type based on a condition, similar to a ternary operator in JavaScript. The basic syntax is:

T extends U ? X : Y

This reads as: "If type T is assignable to type U, then the type is X; otherwise, the type is Y."

Example:

type IsString<T> = T extends string ? true : false;

type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false

Introducing the infer Keyword

The infer keyword is used within the extends clause of a conditional type to declare a type variable that can be inferred from the type being checked. In essence, it allows you to "capture" a part of a type for later use.

Basic Syntax:

type MyType<T> = T extends (infer U) ? U : never;

In this example, if T is assignable to some type, TypeScript will attempt to infer the type of U. If the inference is successful, the type will be U; otherwise, it will be never.

Simple Examples of infer

1. Inferring the Return Type of a Function

A common use case is inferring the return type of a function:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function add(a: number, b: number): number {
  return a + b;
}

type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number

function greet(name: string): string {
  return `Hello, ${name}!`;
}

type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string

In this example, ReturnType<T> takes a function type T as input. It checks if T is assignable to a function that accepts any arguments and returns a value. If it is, it infers the return type as R and returns it. Otherwise, it returns any.

2. Inferring Array Element Type

Another useful scenario is extracting the element type from an array:

type ArrayElementType<T> = T extends (infer U)[] ? U : never;

type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never

Here, ArrayElementType<T> checks if T is an array type. If it is, it infers the element type as U and returns it. If not, it returns never.

Advanced Use Cases of infer

1. Inferring Parameters of a Constructor

You can use infer to extract the parameter types of a constructor function:

type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;

class Person {
  constructor(public name: string, public age: number) {}
}

type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]

class Point {
    constructor(public x: number, public y: number) {}
}

type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]

In this case, ConstructorParameters<T> takes a constructor function type T. It infers the types of the constructor parameters as P and returns them as a tuple.

2. Extracting Properties from Object Types

infer can also be used to extract specific properties from object types using mapped types and conditional types:

type PickByType<T, K extends keyof T, U> = {
  [P in K as T[P] extends U ? P : never]: T[P];
};

interface User {
  id: number;
  name: string;
  age: number;
  email: string;
  isActive: boolean;
}

type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }

type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }

//An interface representing geographic coordinates.
interface GeoCoordinates {
    latitude: number;
    longitude: number;
    altitude: number;
    country: string;
    city: string;
    timezone: string;
}

type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }

Here, PickByType<T, K, U> creates a new type that includes only the properties of T (with keys in K) whose values are assignable to type U. The mapped type iterates over the keys of T, and the conditional type filters out the keys that don't match the specified type.

3. Working with Promises

You can infer the resolved type of a Promise:

type Awaited<T> = T extends Promise<infer U> ? U : T;

async function fetchData(): Promise<string> {
  return 'Data from API';
}

type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string

async function fetchNumbers(): Promise<number[]> {
    return [1, 2, 3];
}

type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]

The Awaited<T> type takes a type T, which is expected to be a Promise. The type then infers the resolved type U of the Promise, and returns it. If T is not a promise, it returns T. This is a built-in utility type in newer versions of TypeScript.

4. Extracting the Type of an Array of Promises

Combining Awaited and array type inference allows you to infer the type resolved by an array of Promises. This is particularly useful when dealing with Promise.all.

type PromiseArrayReturnType<T extends Promise<any>[]> = {
    [K in keyof T]: Awaited<T[K]>;
};


async function getUSDRate(): Promise<number> {
  return 0.0069;
}

async function getEURRate(): Promise<number> {
  return 0.0064;
}

const rates = [getUSDRate(), getEURRate()];

type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]

This example first defines two asynchronous functions, getUSDRate and getEURRate, which simulate fetching exchange rates. The PromiseArrayReturnType utility type then extracts the resolved type from each Promise in the array, resulting in a tuple type where each element is the awaited type of the corresponding Promise.

Practical Examples Across Different Domains

1. E-commerce Application

Consider an e-commerce application where you fetch product details from an API. You can use infer to extract the type of the product data:

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  imageUrl: string;
  category: string;
  rating: number;
  countryOfOrigin: string;
}

async function fetchProduct(productId: number): Promise<Product> {
  // Simulate API call
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        id: productId,
        name: 'Example Product',
        price: 29.99,
        description: 'A sample product',
        imageUrl: 'https://example.com/image.jpg',
        category: 'Electronics',
        rating: 4.5,
        countryOfOrigin: 'Canada'
      });
    }, 500);
  });
}


type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product

function displayProductDetails(product: ProductType) {
  console.log(`Product Name: ${product.name}`);
  console.log(`Price: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}

fetchProduct(123).then(displayProductDetails);

In this example, we define a Product interface and a fetchProduct function that fetches product details from an API. We use Awaited and ReturnType to extract the Product type from the fetchProduct function's return type, allowing us to type-check the displayProductDetails function.

2. Internationalization (i18n)

Suppose you have a translation function that returns different strings based on the locale. You can use infer to extract the return type of this function for type safety:

interface Translations {
  greeting: string;
  farewell: string;
  welcomeMessage: (name: string) => string;
}

const enTranslations: Translations = {
  greeting: 'Hello',
  farewell: 'Goodbye',
  welcomeMessage: (name: string) => `Welcome, ${name}!`,
};

const frTranslations: Translations = {
  greeting: 'Bonjour',
  farewell: 'Au revoir',
  welcomeMessage: (name: string) => `Bienvenue, ${name}!`,
};

function getTranslation(locale: 'en' | 'fr'): Translations {
  return locale === 'en' ? enTranslations : frTranslations;
}

type TranslationType = ReturnType<typeof getTranslation>;

function greetUser(locale: 'en' | 'fr', name: string) {
  const translations = getTranslation(locale);
  console.log(translations.welcomeMessage(name));
}

greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!

Here, the TranslationType is inferred to be the Translations interface, ensuring that the greetUser function has the correct type information for accessing translated strings.

3. API Response Handling

When working with APIs, the response structure can be complex. infer can help extract specific data types from nested API responses:

interface ApiResponse<T> {
  status: number;
  data: T;
  message?: string;
}

interface UserData {
  id: number;
  username: string;
  email: string;
  profile: {
    firstName: string;
    lastName: string;
    country: string;
    language: string;
  }
}

async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
  // Simulate API call
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        status: 200,
        data: {
          id: userId,
          username: 'johndoe',
          email: 'john.doe@example.com',
          profile: {
            firstName: 'John',
            lastName: 'Doe',
            country: 'USA',
            language: 'en'
          }
        }
      });
    }, 500);
  });
}


type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;

type UserProfileType = UserApiResponse['data']['profile'];

function displayUserProfile(profile: UserProfileType) {
  console.log(`Name: ${profile.firstName} ${profile.lastName}`);
  console.log(`Country: ${profile.country}`);
}

fetchUser(123).then((response) => {
  if (response.status === 200) {
    displayUserProfile(response.data.profile);
  }
});

In this example, we define an ApiResponse interface and a UserData interface. We use infer and type indexing to extract the UserProfileType from the API response, ensuring that the displayUserProfile function receives the correct type.

Best Practices for Using infer

Common Pitfalls

Alternatives to infer

While infer is a powerful tool, there are situations where alternative approaches might be more appropriate:

Conclusion

The infer keyword in TypeScript, when combined with conditional types, unlocks advanced type manipulation capabilities. It allows you to extract specific types from complex type structures, enabling you to write more robust, maintainable, and type-safe code. From inferring function return types to extracting properties from object types, the possibilities are vast. By understanding the principles and best practices outlined in this guide, you can leverage infer to its full potential and elevate your TypeScript skills. Remember to document your types, test them thoroughly, and consider alternative approaches when appropriate. Mastering infer empowers you to write truly expressive and powerful TypeScript code, ultimately leading to better software.