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?
Автор: ru_vds