Привет, Хабр! Меня зовут Андрей, я Frontend разработчик.
О статье
Продолжаем погружаться в продвинутый TypeScript. В этой статье рассмотрим conditional types, посмотрим на реализацию с примерами, узнаем какую роль играют ключевые слова extends и infer.
Перед прочтением данной статьи рекомендую ознакомиться с базовыми понятиями и возможностями языка, в этом вам поможет одна из моих прошлых статей:
TypeScript и все что тебе нужно в разработке
Статья предназначена для тех, кто хочет научиться уверенно пользоваться инструментом. Навыки помогут типизировать вам более сложные объекты в разработке крупных проектов.
Навигатор
Conditional types
Conditional types или же "условные типы" позволяют определять типы в зависимости от условия. Если коротко, то это тернарный условный оператор, применяемый на уровне типа, а не на уровне значения. Принцип работы абсолютно такой же, как и в работе с переменными.
Условие ? Выполняем, если условие true : Выполняем, если условие false
type TypeA = { id: string }
type TypeB = { id: number }
type ConditionalType<T> = T extends TypeA ? TypeA : never
type ResultType1 = ConditionalType<TypeA> // TypeA
type ResultType2 = ConditionalType<TypeB> // never
Так же, как и при работе со значениями, а не типами, вы можете вызывать тернарные операторы по цепочке, тем самым расширив возможности условных типов.
Условие ? Если true : Условие ? Если true : Если false
type TypeA = { id: string }
type TypeB = { id: number }
type ConditionalType<T> = T extends TypeA ? TypeA : T extends TypeB ? TypeB : never
type ResultType1 = ConditionalType<TypeA> // TypeA
type ResultType2 = ConditionalType<TypeB> // TypeB
type ResultType3 = ConditionalType<string> // never
Если вам сложно воспринимать примеры на уровне типов, советую поработать в тернарными операторами в песочнице на уровне значений.
Вы могли заметить, что мы используем ключевое слово extends. Как оно работает ?
Extends проверяет, расширяет ли тип T другой данный тип TypeA, другими словами, мы убеждаемся, что значение типа T так же имеет тип TypeA.
Вы можете использовать conditional types для безопасности. Приведу пример:
type TypeA = { id: string }
type TypeB = { id: number }
type SafeType<T extends TypeA> = T['id']
type ResultType1 = SafeType<TypeA> // string
type ResultType2 = SafeType<TypeB> // ERROR: Type 'TypeB' does not satisfy the constraint 'TypeA'.
Такие типы называют constraints types или же "ограничивающие типы". Для этого вида типов можно придумать много применений, например, в связке с typeof мы можем обезопасить себя при разработке от невалидных данных, которые могут попасть в наши методы.
Infer in conditional types
Ключевое слово infer дополняет условные типы и не может использоваться вне расширения. Это ключевое слово позволяет нам определить переменную внутри нашего ограничения, на которую можно ссылаться или возвращать.
Перейдем к примеру:
type TypeA = { id: string }
type TypeB = { id: number }
type InferType<T> = T extends { id: infer P } ? P extends string ? string : number : any
type ResultType1 = InferType<TypeA> // string
type ResultType2 = InferType<TypeB> // number
type ResultType3 = InferType<object> // any
ReturnType
Перед нами открываются большие возможности в типизации нашего проекта.
Рассмотрим еще один пример, где мы реализуем кастомный ReturnType:
type CustomReturnType<T> = T extends (...args: any[]) => infer P ? P : any
type ResultType1 = CustomReturnType<() => void> // void
type ResultType3 = CustomReturnType<() => number> // number
Рассмотрим примеры поинтереснее:
ArrayType
В данном примере мы возвращаем union тип из содержимого массива.
type ArrayType<T> = T extends (infer Item)[] ? Item : T
const arr = [1, '2', null, undefined]
type ResultType = ArrayType<typeof arr> // string | number | null | undefined
FirstArgType
В данном примере мы возвращаем тип первого аргумента функции. Подобное мы можем сделать и со вторым и с третьим аргументом, или же со списком аргументов.
type CustomType<T> = T extends (id: infer ID, ...args: any[]) => unknown ? ID : never
type ResultType1 = CustomType<(id: string) => void> // string
type ResultType3 = CustomType<(id: number) => void> // number
CustomInstanceType
Напишем кастомную реализацию InstanceType из TS.
type CustomInstanceType<T> = T extends new (...args: any[]) => infer P ? P : any
interface ConstructorI {
new (arg: number): string
}
type ResultType1 = CustomInstanceType<ConstructorI> // string
type ResultType2 = InstanceType<ConstructorI> // string
Заключение
TypeScript - это мощный инструмент для разработки, который позволяет улучшить качество вашего кода, сделать его более надежным и легко поддерживаемым. В этой статье мы рассмотрели более сложные приемы в работе с TypeScript.
Не забывайте, что изучение TypeScript - это постоянный процесс, и чем больше вы практикуетесь, тем более уверенно будете использовать его в своих проектах.
Материалы для изучения
Основы
Utility Types
Шпаргалка по TS в картинках
TypeScript и все что тебе нужно в разработке