Введение в условные типы в TypeScript

Условные типы в TypeScript дают нам возможность определять определенные типы на основе логики, как и в других аспектах нашего кода. Они являются полезным инструментом для определения типов в TypeScript.

Они имеют знакомый формат, в котором мы пишем их как condition ? ifConditionTrue : ifConditionFalse — это формат, который уже используется повсеместно в TypeScript и Javascript. Давайте рассмотрим, как они работают.

Как работают условные типы в TypeScript

Давайте рассмотрим упрощенный пример, чтобы понять, как это работает. Здесь значением может быть дата рождения (DOB) или возраст пользователя. Если это дата рождения, то тип должен быть строкой, а если возраст, то числом. Мы определим три типа: Dob, Age и UserAgeInformation.

type Dob = string;
type Age = number;
type UserAgeInformation<T> = T extends number ? number : string;
Вход в полноэкранный режим Выход из полноэкранного режима

Как уже упоминалось, Dob будет строкой, например 12/12/1942, а Age, должно быть число, например 96.

Когда мы определяли UserAgeInformation, мы написали его следующим образом:

type UserAgeInformation<T> = T extends number ? number : string;
Войти в полноэкранный режим Выйти из полноэкранного режима

Где T — аргумент для UserAgeInformation. Мы можем передать сюда любой тип. Тогда мы скажем, что если T расширяет число, то тип будет число. В противном случае, это string. По сути, мы хотим сказать, что если T имеет тип number, то UserAgeInformation должна быть number.

Затем мы можем передать Age в userAgeInformation, если мы хотим, чтобы это было число, и Dob в, если мы хотим, чтобы это была строка:

type Dob = string;
type Age = number;
type UserAgeInformation<T> = T extends number ? number : string;

let userAge:UserAgeInformation<Age> = 100;
let userDob:UserAgeInformation<Dob> = '12/12/1945';
Вход в полноэкранный режим Выйти из полноэкранного режима

Комбинирование условных типов с keyof

Мы можем сделать еще один шаг вперед, проверяя, расширяет ли T объект. Например, допустим, мы управляем бизнесом, который имеет два типа клиентов: лошади и пользователи. Хотя у User есть адрес, у Horse обычно есть только местоположение. Для каждого из них существуют свои форматы адресов, как показано ниже:

type User = {
    age: number,
    name: string,
    address: string
}

type Horse = {
    age: number,
    name: string
}

type UserAddress = {
    addressLine1: string,
    city: string,
    country: string,
}

type HorseAddress = {
    location: 'farm' | 'savanna' | 'field' | 'other'
}
Войти в полноэкранный режим Выйти из полноэкранного режима

В будущем у нас могут быть и другие типы клиентов, поэтому мы можем проверить в общем случае, есть ли у T свойство address. Если да, используйте UserAddress. В противном случае используйте HorseAddress в качестве конечного типа:

type AddressComponents<T> = T extends { address: string } ? UserAddress : HorseAddress

let userAddress:AddressComponents<User> = {
    addressLine1: "123 Fake Street",
    city: "Boston",
    country: "USA"
}

let horseAddress:AddressComponents<Horse> = {
    location: 'farm'
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Когда мы говорим T extends { address: string }, мы проверяем, есть ли у T свойство address. Если да, то мы используем UserAddress. В противном случае мы можем по умолчанию использовать HorseAddress.

Использование T в условных возвратах

Мы можем даже использовать сам T в условных возвратах. В этом примере, поскольку T определен как User, когда мы вызываем его (UserType<User>), myUser имеет тип User, и требует полей, определенных в этом типе (age, name, address):

type User = {
    age: number,
    name: string,
    address: string
}

type Horse = {
    age: number,
    name: string
}

type UserType<T> = T extends { address: string } ? T : Horse

let myUser:UserType<User> = {
    age: 104, 
    name: "John Doe",
    address: "123 Fake Street"
}
Войти в полноэкранный режим Выход из полноэкранного режима

Объединение типов при использовании T в выводах типов

Если мы передадим сюда объединенный тип, то каждый из них будет проверяться отдельно. Например, допустим, мы сделали следующее:

type UserType<T> = T extends { address: string } ? T : string
let myUser:UserType<User | Horse> = {
    age: 104, 
    name: "John Doe",
    address: "123 Fake Street"
}
Войти в полноэкранный режим Выйти из полноэкранного режима

myUser, приведенное выше, фактически становится типом User | string. Это потому, что хотя User проходит условную проверку, Horse не проходит — поэтому он возвращает строку.

Если мы изменим T каким-либо образом (например, сделаем его массивом). Все значения T будут изменены по отдельности. Например, возьмем следующий пример:

type User = {
    age?: number,
    name: string,
    address?: string
}
type Horse = {
    age?: number,
    name: string
}
type UserType<T> = T extends { name: string } ? T[] : never;
//   ^ -- will return the type arguement T as T[], if T contains the property `name` of type `string`
let myUser:UserType<User | Horse> = [{ name: "John" }, { name: "Horse" }]
//  ^ -- becomes User[] | Horse[], since both User and Horse have the property name
Войти в полноэкранный режим Выйти из полноэкранного режима

Здесь мы упростили User и Horse, чтобы они имели только необходимое свойство name. В нашем условном типе оба типа содержат свойство name. Поэтому оба возвращают true, а возвращаемый тип — T[]. Поскольку оба возвращают true, myUser имеет тип User[] | Horse[], поэтому мы можем просто предоставить массив объектов, содержащих свойство name.

Обычно такое поведение нормально, но в некоторых случаях вы можете захотеть вернуть массив либо User, либо Horse. В этом случае, когда мы хотим избежать подобного распределения типов, мы можем добавить скобки вокруг T и { name: string }:

type User = {
    age?: number,
    name: string,
    address?: string
}
type Horse = {
    age?: number,
    name: string
}
type UserType<T> = [T] extends [{ name: string }] ? T[] : never;
//   ^ -- here, we avoid distributing the types, since T and { name: string } are in brackets
let myUser:UserType<User | Horse> = [{ name: "John" }, { name: "Horse" }]
//  ^ -- that means the type is slightly different now - it is (User | Horse)[]
Войти в полноэкранный режим Выход из полноэкранного режима

Благодаря использованию квадратных скобок наш тип теперь преобразован в (User | Horse)[], а не в User[] | Horse[]. Это может быть полезно в некоторых специфических обстоятельствах, и это сложность условных типов, о которой полезно помнить.

Вывод типов с помощью условных типов

Мы также можем использовать ключевое слово infer при использовании условных типов. Предположим, у нас есть два типа, один для массива чисел, а другой для массива строк. В этом простом случае infer выведет типы каждого элемента массива и вернет правильный тип:

type StringArray = string[];
type NumberArray = number[];
type MixedArray = number[] | string[];
type ArrayType<T> = T extends Array<infer Item> ? Item : never;
let myItem1:ArrayType<NumberArray> = 45
//  ^ -- since the items in `NumberArray` are of type `number`, the type of `myItem` is `number`.
let myItem2:ArrayType<StringArray> = 'string'
//  ^ -- since the items in `StringArray` are of type `string`, the type of `myItem` is `string`.
let myItem3:ArrayType<MixedArray> = 'string'
//  ^ -- since the items in `MixedArray` can be `string` or `number, the type of `myItem is `string | number`
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь мы определяем новый аргумент в нашем условном типе под названием Item, который представляет собой элементы внутри Array, который расширяет T. Примечательно, что это работает только если тип, который мы передаем, является массивом, так как мы используем Array<infer Item>.

В случае, если T является массивом, то ArrayType возвращает тип его элементов. Если T не является массивом, то ArrayType будет иметь тип never.

Заключение

Условные типы в TypeScript поначалу могут показаться запутанными, но, по сути, это просто еще один способ упростить написание типов в некоторых конкретных обстоятельствах. Полезно знать, как это работает, если вы когда-нибудь увидите это в репозитории или проекте, или для упрощения своей собственной кодовой базы.

Надеюсь, вам понравилось это руководство. Если да, то вам также может понравиться статья, которую я написал о типе утилиты Record.

Оцените статью
devanswers.ru
Добавить комментарий