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

от автора

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


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

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

let variable; 

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

let variable = 'Hello!'; 

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

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

function getRandomInteger(max: number) {   return Math.floor(Math.random() * max); } 

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

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

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

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

function getProperty<ObjectType, KeyType extends keyof ObjectType>(   object: ObjectType, key: KeyType ) {   return object[key]; } 

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

const dog = {   name: 'Fluffy' }; getProperty(dog, 'name'); 

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

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

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

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

function call<ReturnType>(   functionToCall: (...args: any[]) => ReturnType, ...args: any[] ): ReturnType {   return functionToCall(...args); } 

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

const randomNumber = call(getRandomInteger, 100); 

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

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

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

function call<ArgumentsType extends any[], ReturnType>(   functionToCall: (...args: ArgumentsType) => ReturnType, ...args: ArgumentsType ): ReturnType {   return functionToCall(...args); } 

Теперь мы указали на то, что функция 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<FunctionType extends (...args: any) => ?> = ?; 

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

type FunctionReturnType<ReturnType, FunctionType extends (...args: any) => ReturnType> = ReturnType; FunctionReturnType<number, typeof getRandomInteger>; 

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

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

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

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

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

FunctionReturnType<typeof getRandomInteger>; // 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?

ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/493716/


Комментарии

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

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