В сентябре этого года Microsoft анонсировал TypeScript 4.9 beta. В бета-версии появились любопытные нововведения и исправления: новый оператор, оптимизация производительности, улучшения существующих типов…
Меня зовут Екатерина Семенова, я — фронтенд-разработчик в Surf. Давайте вместе разберём самые интересные фичи этого анонса.
Новый оператор satisfies
TL;DR Позволяет делать неоднородные наборы типов более гибкими.
Чтобы понять, зачем нужен оператор satisfies, рассмотрим пример переменной-записи, где данные имеют смешанный характер. Положим, что значения этой записи могут быть как строками, так и числами.
type ComponentKey = 'component1' | 'component2' | 'component3';
const data: Record<ComponentKey, number | string> = {
component1: 0,
component2: '',
component3: 42,
};
const firstResult = data.component1 + 42;
const secondResult = data.component2.toUpperCase();Если мы попытаемся работать с объектом data далее, неизбежно наткнёмся на ошибки типов:
Operator '+' cannot be applied to types 'string | number' and 'number'.
Property 'toUpperCase' does not exist on type 'string | number'.
Property 'toUpperCase' does not exist on type 'number'.Это объясняется тем, что number | string — это объединение. TypeScript разрешает операцию над объединением только в том случае, если она действительна для каждого члена объединения.
TypeScript 4.9 beta предлагает выход: использовать новый оператор satisfies, который позволит безболезненно указывать, какому именно типу удовлетворяет объект.
type ComponentKey = 'component1' | 'component2' | 'component3';
const data = {
component1: 0,
component2: '',
component3: 42,
} satisfies Record<ComponentKey, number | string>;
const firstResult = data.component1 + 42;
const secondResult = data.component2.toUpperCase();Компиляция проходит без ошибок. Попробуем поменять значение в component2, чтобы убедиться, что новый оператор действительно приводит объект к нужному типу:
type ComponentKey = 'component1' | 'component2' | 'component3';
const data = {
component1: 0,
component2: 44,
component3: 42,
} satisfies Record<ComponentKey, number | string>;
const firstResult = data.component1 + 42;
const secondResult = data.component2.toUpperCase();И, как и ожидалось, увидим ошибку:
Property 'toUpperCase' does not exist on type 'number'.Таким образом мы сохраняем контроль над типами, и работа с записями становится удобнее.
Подробнее про оператор, примеры использования, спорные моменты
Умный in
TL;DR В операторе in станет меньше ошибок при сужении типов.
Оператор in в JavaScript проверяет, существует ли свойство у объекта. Это удобно для данных, чей тип мы не знаем: например, для данных файлов конфигураций.
В TypeScript оператор in часто используется, чтобы проверить, входит ли свойство в объект определённого типа. Пример:
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function isFish(creature: Fish | Bird): creature is Fish {
return 'swim' in creature;
}isFish — это предикат, который определяет принадлежность параметра creature типу Fish. Для этого выполняется проверка: если свойство swim содержится в объекте, то очевидно, что параметр creature относится к типу Fish.
Возвращаемый тип функции isFish помогает при вызове функции неявно привести параметр creature к нужному типу, по сути сузив его:
function action(creature: Fish | Bird) {
if (isFish(creature)) creature.swim(); // Это Fish
else creature.fly(); // А это Bird
}Но что, если тип объекта, в котором проверяется наличие свойства, неизвестен? Что будет, если мы попробуем проверить, существует ли свойство в объекте с заранее неизвестным типом? Рассмотрим следующий пример, более приближенный к жизни:
function getConfigVersion(config: unknown) {
if (config && typeof config === 'object') {
if ('version' in config && typeof config.version === 'string') {
return config.version;
}
}
return undefined;
}В этом примере мы пытаемся получить свойство version из параметра, чей тип невозможно предсказать заранее. В первой проверке неявно приводим config к типу object, во второй убеждаемся, что свойство version типа string есть в объекте. Но почему-то получаем ошибки:
Property 'version' does not exist on type 'object'.
Property 'version' does not exist on type 'object'.В чем дело? Оказывается, оператор in строго ограничен типом, который фактически определяет проверяемую переменную. Тип переменной config уже известен как object, соответственно, ни к чему другому приведение в данном случае невозможно: дальнейшая работа с переменной затруднительна.
TypeScript 4.9-beta исправляет это поведение. Вместо того, чтобы оставлять объект «как есть», добавляет к типу Record<"property-key-being-checked", unknown> . Переменная config после всех проверок будет иметь тип object & Record<"version", unknown>, что позволит функции выполниться без ошибок.
Больше информации по теме — в гитхабе Microsoft
Not a number
TL;DR Прямое сравнение с NaN теперь запрещено.
NaN — это специальное числовое значение, обозначающее что угодно, но не число. NaN — единственное значение в JavaScript, которое при сравнении с самим собой с помощью оператора строгого равенства (===) дает false. То же происходит при сравнении с любыми другими значениями. То есть ничто не может быть равно NaN — даже NaN!
Примеры типов операций, которые возвращают NaN:
Неудачное преобразование чисел. Например,
parseInt("not_a_number").Математическая операция, результат которой не является действительным числом. Например,
Math.sqrt(-1).Метод или выражение, операнд которого является NaN или приводится к нему. Например, 42 * NaN.
То есть наткнуться на NaN в своем коде вполне реально. Это приводит к тому, что разработчики могут случайно сравнить результат операции напрямую с NaN:
parseInt(someValue) !== NaNЧто, как следует из определения, приведёт к логической ошибке, которую можно сразу не заметить: выполнить такую проверку ничто не мешает, и разработчик всегда будет получать true.
Решение проблемы — запрет на прямое сравнение с NaN. В бета-версии TypeScript 4.9 сравнение возможно только через специальный метод Number.isNaN, который позволит избежать логических ошибок при сравнении.
Теперь разработчики видят ошибку:
TS2845: This condition will always return 'true'.
Did you mean '!Number.isNaN(...)'?Отслеживание изменений
TL;DR Изменена стратегия по умолчанию для отслеживания изменений — механизм File System Events.
В более ранних версиях TypeScript для отслеживания изменений использовалась стратегия опроса — pooling: периодическая проверка состояния файла на наличие обновлений. В этом подходе есть плюсы и минусы. Например, такой способ отслеживания изменений считается более надежным и предсказуемым на разных платформах и файловых системах. Однако если кодовая база большая, то отслеживание изменений путём опроса может серьезно повысить нагрузку на ЦП и привести к перерасходу ресурсов.
В TypeScript 4.9 стратегией по умолчанию станет механизм событий файловой системы — File System Events. Он основывается на подписке на событие изменения файлов и выполнение кода только тогда, когда это событие произошло. Таким образом, отпадает необходимость периодических опросов. Для большинства разработчиков это нововведение должно обеспечить гораздо более комфортный опыт при работе в режиме --watch или при работе с редактором на основе TypeScript.
Настроить отслеживание изменений по-прежнему можно с помощью переменных среды и watchOptions.
Подробнее — в хендбуке TypeScript
Что дальше
Команда разработчиков продолжает разработку TypeScript 4.9 и в начале ноября планирует выпустить окончательный релиз. Ждём!
А пока что можно установить себе бета-версию и попробовать новые фичи:
npm install -D typescript@betaСледить за планом итераций TypeScript 4.9.
Почитать про другие изменения бета-версии.
