Skip to main content
Практика завдань

Умовні типи в TypeScript

Що таке умовні типи?

Умовні типи — це конструкція TypeScript, яка дозволяє вибирати тип на основі умови. Вони працюють як тернарні оператори, але для типів.

Синтаксис

typescript
T extends U ? X : Y

Якщо тип T може бути присвоєний типу U, результатом буде тип X, в іншому випадку — тип Y.


Простий приклад

typescript
type IsString<T> = T extends string ? true : false; type A = IsString<string>; // true type B = IsString<number>; // false type C = IsString<'hello'>; // true

Як це працює?

  1. Перевіряє, чи є T підтипом string
  2. Якщо так — повертає true
  3. Якщо ні — повертає false

Практичні приклади

Витягування типу повернення

typescript
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

Фільтрація типів

typescript
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

Перевірка на масив

typescript
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 (кортеж також є масивом)

Розподільчі умовні типи

Коли умовний тип застосовується до об'єднаного типу, він розподіляється по кожному члену об'єднання.

typescript
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[]

Як це працює?

typescript
ToArray<string | number> // Розподіляється як: = ToArray<string> | ToArray<number> = string[] | number[]

Вимкнення розподілу

Використовуйте квадратні дужки, щоб запобігти розподілу:

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

Ключове слово infer

infer дозволяє "витягувати" тип зі структури під час умовної перевірки.

Розгортання типу Promise

typescript
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

Витягування типів аргументів функції

typescript
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

Витягування типу елемента масиву

typescript
type ElementType<T> = T extends (infer E)[] ? E : T; type A = ElementType<string[]>; // string type B = ElementType<number[]>; // number type C = ElementType<boolean>; // boolean

Вкладені умовні типи

typescript
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

Виключає типи з об'єднання:

typescript
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

Extract

Витягує типи з об'єднання:

typescript
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

NonNullable

Видаляє null та undefined:

typescript
type NonNullable<T> = T extends null | undefined ? never : T; type A = NonNullable<string | null>; // string type B = NonNullable<number | undefined | null>; // number

ReturnType

Витягує тип повернення функції:

typescript
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; }

Просунуті патерни

Рекурсивні умовні типи

typescript
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; }; } */

Витягування ключів конкретного типу

typescript
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"

Умовне створення обов'язкових полів

typescript
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

typescript
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; }

Сплющення об'єднаних типів

typescript
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

typescript
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

Значення або функція

typescript
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 обмежує глибину рекурсивних типів, щоб запобігти безкінечним циклам:

typescript
// Може призвести до помилки "Type instantiation is excessively deep" type DeepArray<T, N extends number = 10> = N extends 0 ? T : DeepArray<T[], Decrement<N>>;

Порядок перевірок має значення

typescript
// Порядок важливий type TypeName<T> = T extends any[] ? "array" : // Спочатку перевіряємо масив T extends object ? "object" : // Потім об'єкт T extends string ? "string" : "other"; type A = TypeName<string[]>; // "array" (не "object")

Never в умовних типах

typescript
type A = never extends string ? true : false; // true // never є підтипом будь-якого типу

Порівняння з іншими підходами

До умовних типів

typescript
// Потрібно було використовувати перевантаження function wrap(x: string): string[]; function wrap(x: number): number[]; function wrap(x: any): any[] { return [x]; }

З умовними типами

typescript
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[]

Загальні помилки

Забування про розподільність

typescript
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

typescript
// Неправильно type Wrong<T> = T extends infer U ? U : never; // Безглуздо // Правильно type Correct<T> = T extends Promise<infer U> ? U : T;

Складна логіка в одному типі

typescript
// Погано - важко читати 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 для витягування типів
  • Надавати приклади вбудованих утиліт
  • Реалізовувати власні утиліти на основі умовних типів

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?
Практика завдань