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

a561b9cf0817030d23b0160065feb537.png

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

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 и все что тебе нужно в разработке

© Habrahabr.ru