TypeScript: infer и conditional types. Продвинутый TS на примерах

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