В сентябре этого года 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
Комментарии (23)
erty3
28.10.2022 10:46+1Решение проблемы — запрет на прямое сравнение с NaN
Теперь разработчики видят ошибкуНо ведь к NaN, как было написано, может приводить любая операция и процедура. Каким образом статический анализатор это поймёт? (прошу прощения если вопрос глупый, не спец по JS)
Cubango
28.10.2022 10:56+1речь идёт про TypeScript, тоесть отлавливаться данная проверка будет на этапе транспиляции
iliazeus
29.10.2022 12:16+1Это, во многом, нацелено именно на сравнение с константой
NaN
. Потому что на автомате часто пишутx != NaN
для проверки наNaN
, но эта проверка по правилам IEEE754 всегда истинна. Теперь случаи такой проверки будут подсвечены как ошибки.
NeoCode
Насколько я понимаю, то что Nan!=Nan - требование IEEE 754. Но почему так сделали? ИМХО NaN это обычный none в опциональном типе, а создатели стандарта чисел с плавающей точкой все зачем-то усложнили.
Djaler
NaN и отсутствие числа - разные вещи
0xd34df00d
Чем разные?
То есть, чем структура, описываемая IEEE754, лучше структуры, где число — это какой-нибудь Maybe Float', где у Float' не бывает NaN, и где операции определены как лифтанутые в Maybe?
m03r
Maybe Float
всем лучше! Обнаружил тут крейт Noisy Float, не такой уж непопулярный, который как раз-таки с безопаснымfloat
UPD: безопасный он довольно условно: просто паникует при любом ненормальном числе. Видимо, это поведение более востребовано, чем аккуратный
Option
вокруг почти всегоFr0sT-Brutal
Возможно, потому же, что в SQL NULL <> NULL
PaulIsh
NaN получается в результате разных операций. Например вы что-то поделили на 0 и получили NaN и вы взяли квадратный корень из -1 и получили NaN. Язык не может гарантировать равенство результатов, которые он не может выразить числом.
0xd34df00d
Не понял аргумент. Почему он не может гарантировать равенство результатов? Какая разница, каким путем получен результат?
faiwer
Я думаю имелось ввиду, что /0=∞, а -1^1/2 — мнимое число. Т.е результаты разные, но представлены одним и тем же NaN, который эдакий "всё плохо". И чтобы не сравнивали разные виды "всё плохо" с друг другом...
0xd34df00d
Если
sqrt : Float -> Float
, то результатsqrt (-1)
непредставим, и надо делатьMaybe Float
независимо от причин.PaulIsh
Выражусь более понятно. Вы же согласны, что следующее сравнение не является истинным с точки зрения математики:
А теперь замените левую и правую часть сравнения на NaN. Почему в этом результат должен отличаться.
0xd34df00d
С точки зрения математики у меня справа i, с чего бы мне его заменять на NaN?
PaulIsh
Потому что JS не умеет комплексные числа, и так как они не могут быть выражены через тип number, то мы имеем NaN. И поскольку JS не может корректно представить левую и правую часть типом number он заменяет их на NaN.
Если в перспективе number расширят на комплексные числа или введут новый тип, то и sqrt(-1) перестанет быть NaN, но пока так.
0xd34df00d
Вы у меня спросили про точку зрения математики, я точку зрения математики и ответил.
А 1/0 вообще не число, и тайпчекаться не должно.
m03r
Был бы явный Option, было бы логично:
(Some(5) == Some(5)) == Some(true)
, при этом(Some(5) == None) == None)
и(None == None) = None)
.Но
Option
тут половинчатый, и такое решение выглядит довольно логично. В конце концов,NaN
представляет разные ситуации не-чисел, например,parseInt("blah")
и"".charCodeAt(1)
. Было бы странно говорить, что они равны в контексте сравнения чисел.0xd34df00d
Зачем так делать? Почему не просто True в первом случае, False во втором и True в третьем?
m03r
Хороший вопрос! Для меня сравнение тут возможно трёх видов (
Number
во всех трёх случаях включает неNaN
,Option<Number>
— включает):Option<Number> -> Option<Number> -> Option<Bool>
, когда сравниваются только числа, а не-числа приводят к принципиально другому результату. Это как в SQL с nullable-типами:NULL = NULL
даётNULL
Option<Number> -> Option<Number> -> Bool
по типу SQL-ногоIS NOT DISTINCT FROM
(оно же<=>
в MySQL). Это Ваш вариантOption<Number> -> Option<Number> -> Bool
, в виде модификации первого варианта, когдаSome(true)
приравнивается кtrue
, а всё остальное — кfalse
. Это вариант из IEEE 754. Кстати, преобразованиеSome<Bool>
вBool
здесь по аналогии с работойWHERE
в SQL: отбираются только строки со значением условияTRUE
, аFALSE
иNULL
— нет, происходит эдакое впихивание трёх значенийOption<Bool>
в два значенияBool
.Я думаю о том, что блок с условием
NaN == NaN
не выполнится, по аналогии с тем, как вSQL
не отберутся строки сNULL = NULL
.Кстати, читал тут документацию, и почему-то
NaN ** 0
в Javascript даёт единицу. Вероятно, потому что нулевая степень чего угодно, даже жаренной с грибами картошки, даёт всё равно единицу.0xd34df00d
Я человек простой: вижу монады — думаю о хаскеле. И вот в хаскеле есть инстанс
Eq a => Eq (Maybe a)
, который определён как— то есть, это как раз описанный мной вариант (и я недостаточно знаю SQL, чтобы согласиться или возразить на тему интерпретации через SQL).
Чтобы выразить ваше отношение, я в хаскеле бы написал
liftA2 (==)
, и это даёт ровно ваш вариант:Какое из этих отношений лучше и должно быть «по умолчанию» — вопрос из первых принципов, похоже, не решаемый, но по субъективному опыту
==
удобнее и нужно чаще. Ну, то есть, я сходу не могу придумать, когда бы мне на практике нужно было именно~==~
, а не==
.m03r
~==~
выглядит ровно как сравнение в SQL с учётом nullable, на этот счёт недавно была полемическая статья.Я совершенно согласен, что в целом для
Maybe
обыкновенный==
выглядит удобнее почти во всех случаях.Кстати, в IEEE 754 прочитал (раздел 5.11):
Соответственно, для каждой операции сравнения разный набор этих вариантов превращается в
true
илиfalse
, там ниже таблицы истинности.Как это работает — понятно, а что это с точки зрения типов и как это думать — перестал понимать совсем.