Перейти к содержимому

Infer typescript что это

  • автор:

TypeScript: Выведение типа

TypeScript — типизированный язык. Однако необязательно указывать тип переменной. TypeScript выводит типы переменных, когда нет доступной явной информации в виде аннотации типов.

Типы определяются компилятором TypeScript, когда:

  • Переменные инициализируются.
  • Для параметров установлены значения по умолчанию.
  • Определяются возвращаемые типы функций.
var a = "some text"

Здесь, поскольку мы явно не определяем a: string с аннотацией типа, TypeScript определяет тип переменной на основе значения присвоенного переменной. Значение a является строкой, и, следовательно, тип выводится как string .

Рассмотрим следующий пример:

var a = "some text"; 
var b = 123;
a = b; // Compiler Error: Type 'number' is not assignable to type 'string'

Приведённый выше код показывает ошибку, потому что при выведении типов TypeScript выводит тип переменной a как string и переменную b как number . Когда мы пытаемся присвоить b переменную a , компилятор жалуется, что тип number не может быть присвоен типу string .

Выведение типа в сложных объектах

Могут быть сценарии, в которых объект может быть инициализирован несколькими типами.

var arr = [ 10, null, 30, 40 ];

В приведённом выше примере у нас есть массив со значениями 10 , null , 30 , и, 40 . TypeScript ищет наиболее распространённый тип, чтобы сделать вывод о типе объекта. В этом случае он выбирает тот, который совместим со всеми типами, т.е. number , а также null .

Рассмотрим другой пример:

var arr = [0, 1, "test"];

В этом случае массив имеет значения типа number , а так же тип string . В таких случая компилятор TypeScript ищет наиболее распространённый тип, чтобы сделать вывод о типе объекта, но не находит никакого супер типа, который мог бы охватывать все типы, присутствующие в массиве. В таких случаях компилятор обрабатывает тип как объединение всех типов, присутствующих в массиве. Здесь будет тип (string|number) — это означает, что массив может содержать либо строковые значения, либо числовые значения. Это называется Объединения (union type)

Попробуем добавить новый элемент в массив:

var arr = [0, 1, "test"]; 
arr.push("str")

Компилятор принимает новое значение, поскольку у него тип string , что нормально.

Теперь давайте попробуем добавить в массив новый тип, который ещё не был частью массива:

var arr = [0, 1, "test"]; 
arr.push("str") // OK
arr.push(true); // Compiler Error: Argument of type 'true' is not assignable to parameter of type 'string | number'

Приведённый выше код покажет ошибку компилятора, потому что boolean не является часть объединение (string | number) .

Тип возвращаемого значения функции так же определяется возвращаемым значением. Например:

function sum(a: number, b: number ) 

return a + b;
>
var total: number = sum(10,20); // OK
var str: string = sum(10,20); // Compiler Error

В приведённом выше код тип возвращаемого значения функции sum() — number . Таким образом, результат может быть сохранён в переменной типа number , а не в переменной типа string .

Таким образом, выведение типа полезно при проверке типов, когда нет доступной явной аннотации типов.

Условные типы¶

Определение динамических типов при помощи условных выражений — очень важный момент описания сложных типизированных сценариев, на примере которых в данной главе будет рассмотрен механизм определения условных типов.

Условные типы на практике¶

Условные типы (Conditional Types) — это типы, способные принимать одно из двух значений, основываясь на принадлежности одного типа к другому. Условные типы семантически схожи с тернарным оператором.

T extends U ? T1 : T2 

В блоке выражения с помощью ключевого слова extends устанавливается принадлежность к заданному типу. Если тип, указанный слева от ключевого слова extends , совместим с типом, указанным по правую сторону, то условный тип будет принадлежать к типу T1 , иначе — к типу T2 . Стоит заметить, что в качестве типов T1 и T2 могут выступать, в том числе и другие условные типы, что в свою очередь создаст цепочку условий определения типа.

Помимо того, что невозможно переоценить пользу от условных типов, очень сложно придумать минимальный пример, который бы эту пользу проиллюстрировал. Поэтому в этой главе будут приведены лишь бессмысленные примеры, демонстрирующие принцип их работы.

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
type T0T> = T extends number ? string : boolean; let v0: T05>; // let v0: string let v1: T0'text'>; // let v1: boolean type T1T> = T extends number | string ? object : never; let v2: T15>; // let v2: object let v3: T1'text'>; // let v3: object let v4: T1true>; // let v2: never type T2T> = T extends number ? 'Ok' : 'Oops'; let v5: T25>; // let v5: "Ok" let v6: T2'text'>; // let v6: "oops" // вложенные условные типы type T3T> = T extends number ? 'IsNumber' : T extends string ? 'IsString' : 'Oops'; let v7: T35>; // let v7: "IsNumber" let v8: T3'text'>; // let v8: "IsString" let v9: T3true>; // let v9: "Opps" 

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

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
type T0T> = T extends IAnimal ? 'animal' : T extends IBird ? 'bird' : T extends IRaven ? 'raven' : 'no animal'; type T1T> = T extends IRaven ? 'raven' : T extends IBird ? 'bird' : T extends IAnimal ? 'animal' : 'no animal'; // всегда "animal" let v0: T0IAnimal>; // let v0: "animal" let v1: T0IBird>; // let v1: "animal" let v2: T0IRaven>; // let v2: "animal" // никогда "bird" let v3: T1IRaven>; // let v3: "raven" let v4: T1IBird>; // let v4: "raven" let v5: T1IAnimal>; // let v5: "animal" 

Если в качестве аргумента условного типа выступает тип объединение ( Union , глава Union, Intersection), то условия будут выполняться для каждого типа, составляющего объединенный тип.

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
interface IAnimal  type: string; > interface IBird extends IAnimal  fly(): void; > interface IRaven extends IBird <> type T0T> = T extends IAnimal ? 'animal' : T extends IBird ? 'bird' : T extends IRaven ? 'raven' : 'no animal'; type T1T> = T extends IRaven ? 'raven' : T extends IBird ? 'bird' : T extends IAnimal ? 'animal' : 'no animal'; // всегда "animal" let v0: T0IAnimal | IBird>; // let v0: "animal" let v1: T0IBird>; // let v1: "animal" let v2: T0IRaven>; // let v2: "animal" // никогда "bird" let v3: T1IAnimal | IRaven>; // let v3: "raven" let v4: T1IBird>; // let v4: "raven" let v5: T1IAnimal | IBird>; // let v5: "animal" 

Помимо конкретного типа, в качестве правого (от ключевого слова extends ) операнда также может выступать другой параметр типа.

1 2 3 4
type T0T, U> = T extends U ? 'Ok' : 'Oops'; let v0: T0number, any>; // Ok let v1: T0number, string>; // Oops 

Распределительные условные типы (Distributive Conditional Types)¶

Условные типы, которым в качестве аргумента типа устанавливается объединенный тип ( Union Type , глава Union, Intersection), называются распределительные условные типы ( Distributive Conditional Types ). Называются они так, потому что каждый тип, составляющий объединенный тип, будет распределен таким образом, чтобы выражение условного типа было выполнено для каждого. Это, в свою очередь может определить условный тип как тип объединение.

1 2 3 4 5 6 7 8
type T0T> = T extends number ? 'numeric' : T extends string ? 'text' : 'other'; let v0: T0string | number>; // let v0: "numeric" | "text" let v1: T0string | boolean>; // let v1: "text" | "other" 

Для лучшего понимания процесса, происходящего при определении условного типа в случае, когда аргумент типа принадлежит к объединению, стоит рассмотреть следующий минимальный пример, в котором будет проиллюстрирован условный тип так, как его видит компилятор.

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
// так видит разработчик type T0T> = T extends number ? 'numeric' : T extends string ? 'text' : 'other'; let v0: T0string | number>; // let v0: "numeric" | "text" let v1: T0string | boolean>; // let v1: "text" | "other" // так видит компилятор type T0T> = // получаем первый тип, составляющий union тип (в данном случае number) и начинаем подставлять его на место T number extends number ? 'numeric' // number соответствует number? Да! Определяем "numeric" : T extends string ? 'text' : | 'other' // закончили определять один тип, приступаем к другому, в данном случае string | string extends number ? 'numeric' // string соответствует number? Нет! Продолжаем. : string extends string ? 'text' // string соответствует string? Да! Определяем "text". : 'other'; // Итого: условный тип T0 определен, как "numeric" | "text" 

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

Условные типы позволяют в блоке выражения объявлять переменные, тип которых будет устанавливать вывод типов. Переменная типа объявляется с помощью ключевого слова infer и, как уже говорилось, может быть объявлена исключительно в типе, указанном в блоке выражения, расположенном правее оператора extends .

Это очень простой механизм, который проще сразу рассмотреть на примере.

Предположим, что нужно установить, к какому типу принадлежит единственный параметр функции.

function f(param: string): void <> 

Для этого нужно создать условный тип, в условии которого происходит проверка на принадлежность к типу-функции. Кроме того, аннотация типа единственного параметра этой функции вместо конкретного типа будет содержать объявление переменной типа.

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
type ParamTypeT> = T extends (p: infer U) => void ? U : undefined; function f0(param: number): void <> function f1(param: string): void <> function f2(): void <> function f3(p0: number, p1: string): void <> function f4(param: number[]): void <> let v0: ParamTypetypeof f0>; // let v0: number let v1: ParamTypetypeof f1>; // let v1: string let v2: ParamTypetypeof f2>; // let v2: <> let v3: ParamTypetypeof f3>; // let v3: undefined let v4: ParamTypetypeof f4>; // let v4: number[]. Oops, ожидалось тип number вместо number[] // определяем новый тип, чтобы разрешить последний случай type WithoutArrayParamTypeT> = T extends ( p: (infer U)[] ) => void ? U : T extends (p: infer U) => void ? U : undefined; let v5: WithoutArrayParamTypetypeof f4>; // let v5: number. Ok 

Принципы определения переменных в условных типах, продемонстрированные на примере функционального типа, идентичны и для объектных типов.

1 2 3 4 5
type ParamTypeT> = T extends  a: infer A; b: infer B > ? A | B : undefined; let v: ParamType a: number; b: string >>; // let v: string | number 

Вывод типов в TypeScript с использованием конструкции as const и ключевого слова infer

TypeScript позволяет автоматизировать множество задач, которые, без использования этого языка, разработчикам приходится решать самостоятельно. Но, работая с TypeScript, нет необходимости постоянно использовать аннотации типов. Дело в том, что компилятор выполняет большую работу по выводу типов, основываясь на контексте выполнения кода. Статья, перевод которой мы сегодня публикуем, посвящена достаточно сложным случаям вывода типов, в которых используется ключевое слово infer и конструкция as const .

Основы вывода типов

Для начала взглянем на простейший пример вывода типов.

let variable; 

Переменная, которая объявлена таким способом, имеет тип any . Мы не дали компилятору каких-либо подсказок о том, как мы будем её использовать.

let variable = 'Hello!'; 

Здесь мы объявили переменную и сразу же записали в неё некое значение. Теперь TypeScript может догадаться о том, что эта переменная имеет тип string , поэтому теперь перед нами вполне приемлемая типизированная переменная.

Похожий подход применим и к функциям:

function getRandomInteger(max: number)

В этом коде мы не указываем того, что функция getRandomInteger возвращает число. Но TypeScript-компилятор очень хорошо об этом знает.

Вывод типов в дженериках

Вышеописанные концепции имеют отношение к универсальным типам (дженерикам). Если вы хотите больше узнать о дженериках — взгляните на этот и этот материалы.

При создании универсальных типов можно сделать много всего полезного. Вывод типов делает работу с универсальными типами более удобной и упрощает её.

function getProperty( object: ObjectType, key: KeyType )

При использовании вышеприведённой дженерик-функции нам не нужно в явном виде указывать типы.

const dog = < name: 'Fluffy' >; getProperty(dog, 'name'); 

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

Использование ключевого слова infer

Одна из наиболее продвинутых возможностей TypeScript, которая приходит в голову при разговоре о выводе типов, это — ключевое слово infer .

Рассмотрим пример. Создадим следующую функцию:

function call( functionToCall: (. args: any[]) => ReturnType, . args: any[] ): ReturnType

Вызовем, с помощью этой функции, другую функцию, и запишем то, что она вернёт, в константу:

const randomNumber = call(getRandomInteger, 100); 

Предыдущее выражение позволяет нам получить то, что вернула функция getRandomInteger , которая получила на вход, в качестве верхней границы возвращаемого ей случайного целого числа, 100. Правда, тут имеется одна небольшая проблема. Она заключается в том, что ничто не мешает нам игнорировать типы аргументов функции getRandomInteger .

const randomNumber = call(getRandomInteger, '100'); // здесь нет ошибки 

Так как TypeScript поддерживает spread- и rest-параметры в функциях высшего порядка, мы можем решить эту проблему так:

function call( functionToCall: (. args: ArgumentsType) => ReturnType, . args: ArgumentsType ): ReturnType

Теперь мы указали на то, что функция call может обрабатывать массив аргументов в любой форме, а также на то, что аргументы должны соответствовать ожиданиям переданной ей функции.

Попробуем теперь снова выполнить некорректный вызов функции:

const randomNumber = call(getRandomInteger, '100'); 

Это приводит к появлению сообщения об ошибке:

Argument of type ‘”100″‘ is not assignable to parameter of type ‘number’. 

На само деле, выполнив вышеописанные действия мы просто создали кортеж. Кортежи в TypeScript — это массивы с фиксированной длиной, типы значений которых известны, но не обязаны быть одинаковыми.

type Option = [string, boolean]; const option: Option = ['lowercase', true]; 

Особенности ключевого слова infer

Теперь давайте представим, что нашей целью является не получение того, что возвращает функция, а лишь получение сведений о типе возвращаемых ей данных.

type FunctionReturnType ?> = ?; 

Вышеприведённый тип пока ещё не готов к работе. Нам нужно решить вопрос о том, как определить возвращаемое значение. Тут можно всё описать вручную, но это идёт вразрез с нашей целью.

type FunctionReturnType ReturnType> = ReturnType; FunctionReturnType; 

Вместо того, чтобы делать это самостоятельно, мы можем попросить TypeScript вывести возвращаемый тип. Ключевое слово infer можно использовать только в условных типах. Именно поэтому наш код иногда может оказаться несколько неопрятным.

type FunctionReturnType any> = FunctionType extends (. args: any) => infer ReturnType ? ReturnType : any; 

Вот что происходит в этом коде:

  • Здесь сказано, что FunctionType расширяет (args: any) => any .
  • Мы указываем на то, что FunctionReturnType — это условный тип.
  • Мы проверяем, расширяет ли FunctionType (. args: any) => infer ReturnType .
FunctionReturnType; // number 

Вышеописанное — это настолько распространённая задача, что в TypeScript имеется встроенная утилита ReturnType, которая предназначена для решения этой задачи.

Конструкция as const

Ещё один вопрос, относящийся к выводу типов, заключается в разнице ключевых слов const и let , используемых при объявлении констант и переменных.

let fruit = 'Banana'; const carrot = 'Carrot'; 

Переменная fruit — имеет тип string . Это означает, что в ней можно хранить любое строковое значение.

А константа carrot — это строковой литерал (string literal). Её можно рассматривать как пример подтипа string . В этом PR дано следующее описание строковых литералов: «Тип string literal — это тип, ожидаемым значением которого является строка с текстовым содержимым, эквивалентным такому же содержимому строкового литерала».

Это поведение можно изменить. В TypeScript 3.4 появилась новая интересная возможность, которая называется const assertions (константное утверждение) и предусматривает применение конструкции as const . Вот как выглядит её использование:

let fruit = 'Banana' as const; 

Теперь fruit — это строковой литерал. Конструкция as const оказывается удобной ещё и тогда, когда некую сущность нужно сделать иммутабельной. Рассмотрим следующий объект:

const user = < name: 'John', role: 'admin' >; 

В JavaScript ключевое слово const означает, что нельзя перезаписать то, что хранится в константе user . Но, с другой стороны, можно поменять внутреннюю структуру объекта, записанного в эту константу.

Сейчас объект хранит следующие типы:

const user: < name: string, role: string >; 

Для того чтобы система воспринимала бы этот объект как иммутабельный, можно воспользоваться конструкцией as const :

const user = < name: 'John', role: 'admin' >as const; 

Теперь типы изменились. Строки стали строковыми литералами, а не обычными строками. Но изменилось не только это. Теперь свойства предназначены только для чтения:

const user: < readonly name: 'John', readonly role: 'admin' >; 

А при работе с массивами перед нами открываются ещё более мощные возможности:

const list = ['one', 'two', 3, 4]; 

Тип этого массива — (string | number)[] . Этот массив, используя as const , можно превратить в кортеж:

const list = ['one', 'two', 3, 4] as const; 

Теперь тип этого массива выглядит так:

readonly ['one', 'two', 3, 4] 

Всё это применимо и к более сложным структурам. Рассмотрим пример, который Андерс Хейлсберг привёл в своём выступлении на TSConf 2019:

const colors = [ < color: 'red', code: < rgb: [255, 0, 0], hex: '#FF0000' >>, < color: 'green', code: < rgb: [0, 255, 0], hex: '#00FF00' >>, < color: 'blue', code: < rgb: [0, 0, 255], hex: '#0000FF' >>, ] as const; 

Наш массив colors теперь защищён от изменений, причём, защищены от изменений и его элементы:

const colors: readonly [ < readonly color: 'red'; readonly code: < readonly rgb: readonly [255, 0, 0]; readonly hex: '#FF0000'; >; >, /// . ] 

Итоги

В этом материале мы рассмотрели некоторые примеры использования продвинутых механизмов вывода типов в TypeScript. Здесь использовано ключевое слово infer и механизм as const . Эти средства могут оказаться очень кстати в некоторых особенно сложных ситуациях. Например, тогда, когда нужно работать с иммутабельными сущностями, или при написании программ в функциональном стиле. Если вы хотите продолжить знакомство с этой темой — взгляните на данный материал.

Уважаемые читатели! Пользуетесь ли вы ключевым словом infer и конструкцией as const в TypeScript?

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

Привет, Хабр! Меня зовут Андрей, я Frontend разработчик.

О статье

Продолжаем погружаться в продвинутый TypeScript. В этой статье рассмотрим conditional types, посмотрим на реализацию с примерами, узнаем какую роль играют ключевые слова extends и infer.

Перед прочтением данной статьи рекомендую ознакомиться с базовыми понятиями и возможностями языка, в этом вам поможет одна из моих прошлых статей:
TypeScript и все что тебе нужно в разработке

Статья предназначена для тех, кто хочет научиться уверенно пользоваться инструментом. Навыки помогут типизировать вам более сложные объекты в разработке крупных проектов.

Навигатор

  • conditional types
  • Infer in conditional types
  • ReturnType
  • ArrayType
  • FirstArgType
  • CustomInstanceType
  • Заключение
  • Материалы для изучения

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 — это постоянный процесс, и чем больше вы практикуетесь, тем более уверенно будете использовать его в своих проектах.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *