阿里云主机折上折
  • 微信号
Current Site:Index > Combining generics with conditional types

Combining generics with conditional types

Author:Chuan Chen 阅读数:57684人阅读 分类: TypeScript

Combining Generics with Conditional Types

The combination of generics and conditional types in TypeScript greatly enhances the flexibility of the type system. Generics enable code reuse, while conditional types dynamically determine output types based on input types. When combined, the type system can handle more complex scenarios, such as type filtering, type mapping, or type inference.

Review of Generics Basics

Generics are a crucial tool in TypeScript for creating reusable components. They allow defining functions, interfaces, or classes without specifying concrete types in advance, deferring the type specification until usage.

function identity<T>(arg: T): T {
  return arg;
}

let output = identity<string>("hello");

Generic constraints can limit the range of generic parameter types:

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

Basics of Conditional Types

Conditional types select different type branches based on type relationships, with the syntax T extends U ? X : Y.

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

type A = IsString<string>; // true
type B = IsString<number>; // false

Conditional types are often used for type operations:

type NonNullable<T> = T extends null | undefined ? never : T;

Combining Generics with Conditional Types

Combining generics with conditional types allows for the creation of more flexible type utilities. A classic example is the Extract and Exclude utility types:

type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;

This combination enables filtering specific types from union types based on conditions:

type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Exclude<"a" | "b" | "c", "a" | "f">; // "b" | "c"

Distributive Conditional Types

When conditional types operate on union types, they trigger distributive behavior, meaning the condition is applied separately to each member of the union:

type ToArray<T> = T extends any ? T[] : never;
type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]

To avoid distributive behavior, wrap the types in square brackets:

type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Arr = ToArrayNonDist<string | number>; // (string | number)[]

Type Inference in Conditional Types

In the extends clause of a conditional type, the infer keyword can declare type variables to extract types:

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

function foo() { return 123; }
type FooReturn = ReturnType<typeof foo>; // number

This method can also extract function parameter types:

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

function bar(x: string, y: number) {}
type BarParams = Parameters<typeof bar>; // [string, number]

Recursive Conditional Types

Conditional types can recursively reference themselves to handle nested structures:

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

const obj = {
  a: 1,
  b: {
    c: 2,
    d: {
      e: 3
    }
  }
};

type ReadonlyObj = DeepReadonly<typeof obj>;
/*
{
  readonly a: number;
  readonly b: {
    readonly c: number;
    readonly d: {
      readonly e: number;
    };
  };
}
*/

Template Literal Types with Conditional Types

Combining template literal types enables more powerful type operations:

type GetterName<T extends string> = `get${Capitalize<T>}`;

type NameGetters = GetterName<'name' | 'age'>; // "getName" | "getAge"

Further combining with conditional types allows for complex transformations:

type MethodNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

type Methods = MethodNames<{
  id: number;
  getName(): string;
  getAge(): number;
}>; // "getName" | "getAge"

Type Constraints in Conditional Types

In conditional types, constraints can be added to infer-deduced types:

type FirstIfString<T> = 
  T extends [infer S, ...unknown[]] 
    ? S extends string 
      ? S 
      : never 
    : never;

type A = FirstIfString<["hello", 42]>; // "hello"
type B = FirstIfString<[42, "hello"]>; // never

Practical Type Examples

Combining generics and conditional types allows for the creation of many utility type tools:

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

type Nested = Array<Array<number>>;
type Flat = Flatten<Nested>; // Array<number>

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

type Promised = Promise<string>;
type Unwrapped = PromiseType<Promised>; // string

Conditional Types and Function Overloads

Conditional types can simplify function overload declarations:

function process<T extends string | number>(
  input: T
): T extends string ? string : number {
  return typeof input === "string" ? input.toUpperCase() : input.toFixed(2);
}

const str = process("hello"); // string
const num = process(3.14159); // number

Type Predicates and Conditional Types

Combining type predicates enables more precise type guards:

type IsArray<T> = T extends any[] ? true : false;

function assertArray<T>(value: T): asserts value is IsArray<T> extends true ? T : never {
  if (!Array.isArray(value)) {
    throw new Error("Not an array");
  }
}

const unknownValue: unknown = [1, 2, 3];
assertArray(unknownValue);
// Here, unknownValue is inferred as any[]

Conditional Types and Mapped Types

Combining mapped types enables more complex type transformations:

type PartialByKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

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

type PartialUser = PartialByKeys<User, 'name' | 'age'>;
/*
{
  id: number;
  name?: string;
  age?: number;
}
*/

Conditional Types and Indexed Access

Conditional types can be combined with indexed access types for dynamic type queries:

type PropType<T, K> = 
  K extends keyof T ? T[K] :
  K extends `${infer F}.${infer R}` ? 
    F extends keyof T ? 
      PropType<T[F], R> : 
      never :
    never;

interface User {
  name: string;
  address: {
    street: string;
    city: string;
  };
}

type StreetType = PropType<User, 'address.street'>; // string

Conditional Types and never

The never type has special behavior in conditional types and is often used for type exclusion:

type ExcludeNever<T> = T extends never ? never : T;

type WithoutNever = ExcludeNever<string | number | never>; // string | number

Conditional Types and Function Types

Conditional types can distinguish between different function types:

type AsyncFunction<T> = T extends (...args: any[]) => Promise<any> ? T : never;

function isAsync<T>(fn: T): fn is AsyncFunction<T> {
  return fn instanceof Function && fn.constructor.name === "AsyncFunction";
}

async function asyncFn() {}
function syncFn() {}

const test1 = isAsync(asyncFn); // true
const test2 = isAsync(syncFn); // false

Conditional Types and Classes

Conditional types can also be applied to class type checks:

type Constructor<T> = new (...args: any[]) => T;

class Animal {}
class Dog extends Animal {}

type IsAnimal<T> = T extends Animal ? true : false;

type Test1 = IsAnimal<Dog>; // true
type Test2 = IsAnimal<string>; // false

Conditional Types and Variadic Tuples

Combining with variadic tuple types enables more flexible type operations:

type Join<T extends unknown[], D extends string> =
  T extends [] ? '' :
  T extends [infer F] ? F :
  T extends [infer F, ...infer R] ? `${F & string}${D}${Join<R, D>}` :
  string;

type Path = Join<['user', 'profile', 'name'], '/'>; // "user/profile/name"

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.