TypeScript 4.9: что нас ожидает
В сентябре этого года Microsoft анонсировал TypeScript 4.9 beta. В бета-версии появились любопытные нововведения и исправления: новый оператор, оптимизация производительности, улучшения существующих типов…
Меня зовут Екатерина Семенова, я — фронтенд-разработчик в Surf. Давайте вместе разберём самые интересные фичи этого анонса.
Новый оператор satisfies
TL;DR Позволяет делать неоднородные наборы типов более гибкими.
Чтобы понять, зачем нужен оператор satisfies, рассмотрим пример переменной-записи, где данные имеют смешанный характер. Положим, что значения этой записи могут быть как строками, так и числами.
type ComponentKey = 'component1' | 'component2' | 'component3';
const data: Record = {
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;
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;
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