Загрузка...
Загрузка...
Conditional Types (Условные типы) — это конструкция в TypeScript, которая позволяет выбирать тип на основе условия. Работают по принципу тернарного оператора, но для типов.
T extends U ? X : Y
Если тип T можно присвоить типу U, то результат — тип X, иначе — тип Y.
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<'hello'>; // true
T подтипом stringtruefalsetype ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUserName() {
return 'John';
}
function getUserAge() {
return 25;
}
type NameType = ReturnType<typeof getUserName>; // string
type AgeType = ReturnType<typeof getUserAge>; // number
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>; // string
type B = NonNullable<number | undefined>; // number
type C = NonNullable<boolean | null | undefined>; // boolean
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<number[]>; // true
type B = IsArray<string>; // false
type C = IsArray<[1, 2, 3]>; // true (tuple тоже массив)
Когда условный тип применяется к union type, он распределяется по каждому члену объединения.
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
// string extends any ? string[] : never | number extends any ? number[] : never
// string[] | number[]
ToArray<string | number>
// Распределяется как:
= ToArray<string> | ToArray<number>
= string[] | number[]
Используйте квадратные скобки для предотвращения распределения:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type A = ToArrayNonDist<string | number>; // (string | number)[]
infer позволяет "извлечь" тип из структуры во время проверки условия.
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string>>; // string
type B = Unwrap<Promise<number>>; // number
type C = Unwrap<boolean>; // boolean
type FirstArg<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
function test(name: string, age: number) {
return { name, age };
}
type First = FirstArg<typeof test>; // string
type ElementType<T> = T extends (infer E)[] ? E : T;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<boolean>; // boolean
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type A = TypeName<string>; // "string"
type B = TypeName<42>; // "number"
type C = TypeName<() => void>; // "function"
type D = TypeName<{}>; // "object"
TypeScript предоставляет множество встроенных утилит, реализованных через условные типы.
Исключает типы из union:
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
type B = Exclude<string | number, string>; // number
Извлекает типы из union:
type Extract<T, U> = T extends U ? T : never;
type A = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'
type B = Extract<string | number, number>; // number
Удаляет null и undefined:
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>; // string
type B = NonNullable<number | undefined | null>; // number
Извлекает тип возвращаемого значения функции:
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
function getUserData() {
return { name: 'John', age: 25 };
}
type UserData = ReturnType<typeof getUserData>;
// { name: string; age: number; }
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
interface User {
name: string;
address: {
city: string;
country: string;
};
}
type ReadonlyUser = DeepReadonly<User>;
/*
{
readonly name: string;
readonly address: {
readonly city: string;
readonly country: string;
};
}
*/
type KeysOfType<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T];
interface User {
id: number;
name: string;
age: number;
email: string;
}
type StringKeys = KeysOfType<User, string>; // "name" | "email"
type NumberKeys = KeysOfType<User, number>; // "id" | "age"
type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;
interface User {
name?: string;
age?: number;
email?: string;
}
type UserWithName = RequireKeys<User, 'name'>;
// { name: string; age?: number; email?: string; }
type ApiResponse<T> = T extends { data: infer D }
? D
: T;
interface SuccessResponse {
data: { id: number; name: string };
status: 'success';
}
type Data = ApiResponse<SuccessResponse>;
// { id: number; name: string; }
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>; // string
type B = Flatten<number[][]>; // number[]
type C = Flatten<(string | number)[]>; // string | number
type UnwrapPromise<T> = T extends Promise<infer U>
? UnwrapPromise<U>
: T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<Promise<number>>>; // number
type C = UnwrapPromise<Promise<Promise<Promise<boolean>>>>; // boolean
type ValueOrFunction<T> = T | (() => T);
type Resolve<T> = T extends (...args: any[]) => infer R ? R : T;
type A = Resolve<string>; // string
type B = Resolve<() => number>; // number
TypeScript ограничивает глубину рекурсивных типов для предотвращения бесконечных циклов:
// Может привести к ошибке "Type instantiation is excessively deep"
type DeepArray<T, N extends number = 10> =
N extends 0 ? T : DeepArray<T[], Decrement<N>>;
// Порядок важен
type TypeName<T> =
T extends any[] ? "array" : // Сначала проверяем массив
T extends object ? "object" : // Потом объект
T extends string ? "string" :
"other";
type A = TypeName<string[]>; // "array" (не "object")
type A = never extends string ? true : false; // true
// never является подтипом любого типа
// Приходилось использовать перегрузки
function wrap(x: string): string[];
function wrap(x: number): number[];
function wrap(x: any): any[] {
return [x];
}
type Wrap<T> = T extends any ? T[] : never;
function wrap<T>(x: T): Wrap<T> {
return [x] as Wrap<T>;
}
const a = wrap('hello'); // string[]
const b = wrap(42); // number[]
type WrapInArray<T> = T extends any ? T[] : never;
type A = WrapInArray<string | number>; // string[] | number[]
// Ожидали (string | number)[], но получили union массивов
// Правильно:
type WrapInArray<T> = [T] extends [any] ? T[] : never;
type B = WrapInArray<string | number>; // (string | number)[]
// Неправильно
type Wrong<T> = T extends infer U ? U : never; // Бессмысленно
// Правильно
type Correct<T> = T extends Promise<infer U> ? U : T;
// Плохо - сложно читать
type Complex<T> = T extends string ? T extends `${infer F}${infer R}` ? F extends 'a' ? true : false : false : false;
// Хорошо - разбить на части
type StartsWithA<T> = T extends `a${string}` ? true : false;
type IsString<T> = T extends string ? true : false;
type Complex<T> = IsString<T> extends true ? StartsWithA<T> : false;
Conditional Types:
inferНа собеседовании:
Важно уметь:
T extends U ? X : Yinfer для извлечения типов