Вывод типов в 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/#habracut

Интересные статьи

Интересные статьи

В Universal Render Pipeline, создавая свои RendererFeature, можно легко расширить возможности отрисовки. Добавление новых проходов в конвеер рендеринга позволяет создавать различные эффекты. В эт...
Одна из важнейших задач при изучении иностранного языка – это не только практика с грамматикой, но и расширение словарного запаса. Чем больше слов вы знаете, тем в большем количестве ситуаций...
Компании растут и меняются. Если для небольшого бизнеса легко прогнозировать последствия любых изменений, то у крупного для такого предвидения — необходимо изучение деталей.
Этот пост будет из серии, об инструментах безопасности, которые доступны в Битриксе сразу «из коробки». Перечислю их все, скажу какой инструмент в какой редакции Битрикса доступен, кратко и не очень р...
Не так давно я столкнулся с довольно простой и одновременно интересной задачей: реализация read-only терминала в веб приложении. Интереса задаче придавали три важных аспекта: поддержка основны...