Привет, Хабр! Меня зовут Андрей, я 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 и все что тебе нужно в разработке

Комментарии (2)


  1. yarrrman
    04.12.2023 05:34
    +5

    Проблема этой статьи и прошлой, что есть пример синтаксиса конструкций, но нет примера зачем их нужно применять.

    Если знаю, что мне нужен infer или ArrayType - я и так найду их синтаксис. А вот примеры где их использовать были бы интересны.


    1. amas1ov Автор
      04.12.2023 05:34

      В последующих статьях уделю большее внимание этой проблеме, спасибо!

      Статьи носят ознакомительный характер, в данной - разбираются вещи, которые явно в повседневной разработке не задействованы, поэтому если при разработке вы столкнетесь с проблемой, то вряд ли, не зная о существовании infer, вы оперативно сможете решите свою проблему. Я так это вижу, но реальные примеры из проектов, которые решают реальные проблемы, стоит добавить при разборе инструмента абсолютно точно)