TypeScript: Advanced TypeScript Generics

Last updated: 2026-08-26

Now that you’ve mastered the basics of generics, this lesson moves on to more advanced topics—conditional types, infer, and mapped types. These are core tools for type-based programming in TypeScript and form the foundation for understanding the source code of built-in utility types.

1. Conditional Types

Conditional types select different results based on type conditions—similar to the ternary operator at the type level:

(1) Basic Syntax

TYPESCRIPT
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;   // "yes"
type B = IsString<number>;   // "no"
type C = IsString<"hello">;  // "yes" ("hello" is a string subtype of)

(2) Conditional Types and Union Types (Distributed)

When T is a union type, the conditional type evaluates each member separately in a "distributed" manner:

TYPESCRIPT
type ToString<T> = T extends string ? "string" : "other";

// T = string | number → Evaluate each one separately → "string" | "other"
type Result = ToString<string | number>;  // "string" | "other"

(3) Disable distributed behavior

Enclose the code in [T] to disable distributed evaluation:

TYPESCRIPT
type IsNever<T> = [T] extends [never] ? "yes" : "no";

type A = IsNever<never>;           // "yes"
type B = IsNever<string | never>;  // "no"(No longer available)

▶ Example: Type-Safe Array Flattening

TYPESCRIPT
type Flatten<T> = T extends Array<infer U> ? U : T;

type A = Flatten<string[]>;        // string
type B = Flatten<number>;          // number
type C = Flatten<boolean[]>;       // boolean
▶ Try it Yourself

Output:

TEXT 📖 Display only
42


2. The infer keyword

infer "Inferring" a type variable in a type condition—it is one of the most powerful tools in TypeScript type programming:

(1) Inferring the Type of Array Elements

TYPESCRIPT
type ArrayElement<T> = T extends (infer E)[] ? E : never;

type A = ArrayElement<string[]>;   // string
type B = ArrayElement<number[]>;   // number
type C = ArrayElement<string>;     // never(string Not an array)

(2) Inferring the return type of a function

TYPESCRIPT
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type A = GetReturnType<() => string>;          // string
type B = GetReturnType<(x: number) => boolean>; // boolean
type C = GetReturnType<(x: string) => void>;    // void
type D = GetReturnType<string>;                  // never(string Not a function)

(3) Inferring Function Parameter Types

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

type A = GetParameters<(a: string, b: number) => void>;  // [string, number]
type B = GetParameters<() => void>;                        // []
type C = GetParameters<(x: boolean) => string>;            // [boolean]

(4) Inferring the Type of a Promise's Resolved Value

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

type A = Awaited<Promise<string>>;     // string
type B = Awaited<Promise<number[]>>;   // number[]
type C = Awaited<string>;              // string(No Promise,Return directly)

// nested Promise——Recursive Unpacking
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;

type D = DeepAwaited<Promise<Promise<string>>>;  // string


3. Mapped Types

Mapping Types: Creating a New Type Based on an Old One—Applying Transformations to Each Property:

(1) Basic Syntax

TYPESCRIPT
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

interface User {
  name: string;
  age: number;
  email: string;
}

type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }

type OptionalUser = Optional<User>;
// { name?: string; age?: number; email?: string }

(2) Mapping Modifiers

Use +/- to add or remove modifiers:

TYPESCRIPT
// Remove readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

// Remove (optional)(?)
type Required2<T> = {
  [K in keyof T]-?: T[K];
};

interface Config {
  readonly host: string;
  readonly port: number;
  debug?: boolean;
}

type MutableConfig = Mutable<Config>;
// { host: string; port: number; debug?: boolean }  —— readonly Removed

type RequiredConfig = Required2<Config>;
// { readonly host: string; readonly port: number; debug: boolean }  —— ? Removed

(3) Key Remapping,TypeScript 4.1+)

TYPESCRIPT
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

(4) Filter Properties

TYPESCRIPT
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Mixed {
  name: string;
  age: number;
  email: string;
  active: boolean;
}

type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string }  —— Keep only string Type Properties

▶ Example: Conditional Types with Mapped Types

Output:

TEXT 📖 Display only
42
TYPESCRIPT
type Stringify<T> = {
  [K in keyof T]: T[K] extends number ? string : T[K];
};

interface Stats {
  count: number;
  label: string;
  active: boolean;
}

type StringifiedStats = Stringify<Stats>;
// { count: string; label: string; active: boolean }
// Only number properties become string

let display: StringifiedStats = {
  count: "42",
  label: "users",
  active: true
};

console.log(display.count);   // "42"

Output:

TEXT 📖 Display only
42

▶ Example: Building a Type-Safe API Client

Output:

TEXT 📖 Display only
42
TYPESCRIPT
// Definition API Routing Type
type ApiRoutes = {
  "/users": { response: { id: number; name: string }[] };
  "/users/:id": { response: { id: number; name: string }; params: { id: number } };
  "/posts": { response: { id: number; title: string }[] };
};

// Extract Response Type Based on Route
type ApiResponse<R extends keyof ApiRoutes> = ApiRoutes[R]["response"];

type UsersResponse = ApiResponse<"/users">;
// { id: number; name: string }[]

type UserResponse = ApiResponse<"/users/:id">;
// { id: number; name: string }

// Type-safe fetch Function
function fetchApi<R extends keyof ApiRoutes>(
  route: R,
  ...args: "params" extends keyof ApiRoutes[R]
    ? [params: ApiRoutes[R]["params"]]
    : []
): Promise<ApiResponse<R>> {
  // Implementation Omitted
  return {} as any;
}

// Automatically infer parameters and return types at runtime
  // let users = fetchApi("/users");           // No parameters required
  // let user = fetchApi("/users/:id", { id: 1 });  // Must be provided params

Output:

TEXT 📖 Display only
Type-safe API client — no runtime output (compile-time type checking only)


4. Default Values for Generics

Generic type parameters can have default values—the default type is used when the type cannot be inferred or does not need to be specified:

TYPESCRIPT
interface PaginatedResult<T, PageSize = 10> {
  data: T[];
  total: number;
  pageSize: PageSize;
}

// Use the default values
type UserResult = PaginatedResult<User>;
// data: User[]; total: number; pageSize: 10

// Override the default value
type CustomResult = PaginatedResult<User, 20>;
// data: User[]; total: number; pageSize: 20

(1) Constraints on Default Values

Type parameters with default values must follow parameters without default values:

TYPESCRIPT
// ✅ Correct
type A<T, U = string> = { first: T; second: U };

// ❌ Error——Those with default values U When there is no default value, T Previous
// type B<U = string, T> = { first: T; second: U };


5. Covariance and Invariance

This is one of the most confusing advanced concepts in the TypeScript type system—you need to understand it to grasp why some assignments are valid and others are not.

(1) Covariant

"If A is a subtype of B, then Container<A> is also a subtype of Container<B>"—Changes in the same direction:

TYPESCRIPT
// string is a subtype of string | number
// string[] Me too (string | number)[] subtypes of → Covariation
let strings: string[] = ["a", "b"];
let mixed: (string | number)[] = strings;  // ✅ Covariant Safety

(2) Adjoint (Contravariant)

Function parameter types are inverse—"If A is a subtype of B, then (B => void) is a subtype of (A => void)"—the relationship works in the opposite direction:

TYPESCRIPT
// string is a subtype of string | number
// (string | number => void) is a subtype of (string => void) → Inversion

type StringHandler = (arg: string) => void;
type MixedHandler = (arg: string | number) => void;

let mixedHandler: MixedHandler = (arg) => console.log(arg);
let stringHandler: StringHandler = mixedHandler;  // ✅ Inversion——Functions that handle broader types can be assigned to variables that handle narrower types.

(3) Why are the function arguments reversed?

TYPESCRIPT
// If the parameter is covariant——Unsafe
let dogHandler: (dog: Dog) => void = (dog) => dog.bark();
let animalHandler: (animal: Animal) => void = dogHandler;  // ❌ Danger!
// Call animalHandler(cat) → dog.bark() called on cat → Runtime Error

// Inverting is the safe option——Functions that handle broader types can safely handle narrower types.
💡 Quick Takeaway: Function return values are covariant (more specific return type safety), while function parameters are contravariant (more general parameter type safety). TypeScript strictly enforces contravariant checks in strictFunctionTypes mode.


❓ FAQ

Q What is the difference between infer and the generic T?
A The generic T is a type parameter provided by the caller, while infer is a type variable automatically inferred within a conditional type. T is "you tell me," and infer is "I infer it myself." infer can only be used in the extends clause of a conditional type.
Q What is the relationship between mapping types and utility types (such as Partial and Required)?
A Utility types are implemented using mapping types. Partial, Required, Readonly, Pick, Omit, and others are all built-in mapping types in TypeScript. Understanding how mapping types work will help you understand how these utility types function, and you’ll also be able to create your own custom utility types.
Q Are covariance and contravariance important in everyday development?
A Most of the time, you don’t need to think about them—TypeScript’s type checking handles them automatically. You only need to understand them when writing generic functions, higher-order functions, or type tools. Beginners should just familiarize themselves with the concepts and delve deeper only when they encounter type mismatches in function assignments.
Q When is a conditional type "distributed"?
A When the T in a conditional type is a bare type parameter (not wrapped in a tuple, object, etc.) and is a union type, the evaluation is distributed. Wrapping it in [T] extends [U] disables distribution. The IsNever example is the most common scenario where distribution needs to be disabled—never is skipped during distribution when it is a union member.

📖 Summary

📝 Exercises

  1. Basic Problem (Difficulty ⭐): Implement IsArray<T> using conditional types—if T is an array type, return true; otherwise, return false. Test IsArray<string[]> and IsArray<number>.
  2. Advanced Problem (Difficulty ⭐⭐): Implement Stringify<T> using a mapping type—convert the types of all an object’s properties to string. For example, { age: number }{ age: string }.
  3. Challenge Problem (Difficulty ⭐⭐⭐): Use infer and conditional types to implement DeepPromise<T>—recursively unwrap nested Promises until a non-Promise type is obtained. The test case DeepPromise<Promise<Promise<Promise<number>>>> should return a number.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏