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

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

mnv8tbxxnlpuwwe-cwl3sfcrz8s.jpeg

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


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

let variable;


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

let variable = 'Hello!';


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

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

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


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

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


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

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

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


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

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


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

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


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

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

function call(
  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(
  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 ?> = ?;


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

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?

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru