Умовні типи в TypeScript
Що таке умовні типи?
Умовні типи — це конструкція 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підтипомstring - Якщо так — повертає
true - Якщо ні — повертає
false
Практичні приклади
Витягування типу повернення
type 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 (кортеж також є масивом)Розподільчі умовні типи
Коли умовний тип застосовується до об'єднаного типу, він розподіляється по кожному члену об'єднання.
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
infer дозволяє "витягувати" тип зі структури під час умовної перевірки.
Розгортання типу Promise
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 надає багато вбудованих утиліт, реалізованих через умовні типи.
Exclude
Виключає типи з об'єднання:
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
type B = Exclude<string | number, string>; // numberExtract
Витягує типи з об'єднання:
type Extract<T, U> = T extends U ? T : never;
type A = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'
type B = Extract<string | number, number>; // numberNonNullable
Видаляє null та undefined:
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>; // string
type B = NonNullable<number | undefined | null>; // numberReturnType
Витягує тип повернення функції:
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; }Практичні випадки використання
Допомога для відповіді API
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Тип ланцюга Promise
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")Never в умовних типах
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)[], але отримано об'єднання масивів
// Правильно:
type WrapInArray<T> = [T] extends [any] ? T[] : never;
type B = WrapInArray<string | number>; // (string | number)[]Неправильне використання infer
// Неправильно
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;Висновок
Умовні типи:
- Дозволяють створювати гнучкі та повторно використовувані типи
- Основи багатьох вбудованих утиліт TypeScript
- Розподіляються по об'єднаних типах (розподільчі)
- Підтримують витягування типів через
infer - Можуть бути рекурсивними (з обмеженнями)
- Критично важливі для просунутого типізації
На співбесідах:
Важливо вміти:
- Пояснити синтаксис
T extends U ? X : Y - Показати різницю між розподільчими та нерозподільчими типами
- Використовувати
inferдля витягування типів - Надавати приклади вбудованих утиліт
- Реалізовувати власні утиліти на основі умовних типів
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.