release

3.9

Улучшение вывода типа для Promise.all

В последних версиях TypeScript (начиная с версии 3.7) были обновлены декларации для таких методов как Promise.all и Promise.race. Но к сожалению это привело к неожиданным результатам в работе вывода типа, что более всего стало очевидно если в выводе участвуют null или undefined.

ts
interface Foodstuff{ isExpirationDate():boolean; } interface Milk extends Foodstuff { } interface Coffee extends Foodstuff { } async function factory(milkOrder: Promise<Milk>, coffeeOrder: Promise<Coffee | undefined>) { let [milk, coffee] = await Promise.all([milkOrder, coffeeOrder]); /** * [error] Object is possibly 'undefined'. * [сейчас] let milk: Milk | undefined * [должно] let milk: Milk * * [ERROR] Ошибочное поведение! */ milk.isExpirationDate(); /** * [error] Object is possibly 'undefined'. * [сейчас] let milk: Coffee | undefined * * [Ok] Ожидаемое\правильное поведение! */ coffee.isExpirationDate(); }

Поскольку данное поведение ошибочно, начиная с версии 3.9 оно было исправлено должным образом.

ts
// ... async function factory(milkOrder: Promise<Milk>, coffeeOrder: Promise<Coffee | undefined>) { let [milk, coffee] = await Promise.all([milkOrder, coffeeOrder]); milk.isExpirationDate(); // Ok! let milk: Milk coffee.isExpirationDate(); // Error! let coffee: Coffee | undefined }

Сокращение скорости компиляции

Работа с такими пакетами как material-ui и styled-components, чья компиляция занимает гораздо больше времени чем хотелось бы, подтолкнула разработчиков языка TypeScript на серию точечных оптимизаций, если быть конкретнее, то шести, каждая из которых сократила время компиляции от 5% до 10%. По словам разработчиков время сборки\редактирования material-ui сократилось на 40%. Кроме того, оптимизации затронули механизм изменения путей для импортов\экспортов при изменении импортируемых\экспортируемых файлов.

Комментарная директива @ts-expect-error

Поскольку разработка на языке TypeScript неразрывно связанна с JavaScript в некоторых моментах может возникать разногласия.

Представьте ситуацию при которой необходимо покрыть тестами функцию принимающую на вход строковой параметр и кроме того выполняющей в своем теле его валидацию времени выполнения.

ts
function isStringAssert(valid:boolean): asserts valid { if(!valid){ throw new Error(`...`); } } function action(value: string){ isStringAssert(typeof value === "string"); // валидация времени выполнения // некоторый код... }

Поскольку лучшие практики тестирования предполагают написание таких тестов которые по своей природе не должны пройти, тестировщик пишущий тесты также на TypeScript при попытке протестировать ошибку время выполнения (assert) столкнется с проблемой время компиляции, так как компилятор не позволит скомпилировать код выявив несоответствие типов.

ts
// файл .ts function isStringAssert(valid:boolean): asserts valid { if(!valid){ throw new Error(`...`); } } function action(value: string){ isStringAssert(typeof value === "string"); // валидация времени выполнения // некоторый код... } // ...где-то в .ts.spec /** * [error] Argument of type '5' is not assignable to parameter of type 'string'. * Компилятор TypeScript не позволяет скомпилировать код имеющий ошибки вызванные * несоответствие типов и тем самым препятствует тестированию кода времени выполнения. */ action(5);

Чтобы разрешить сложившуюся ситуацию начиная с текущей версии была введена комментарная директива // @ts-expect-error. Новая комментарная директива заставляет компилятор подавлять сообщение об ошибке в случае её возникновения, но при отсутствии необходимости сама становится её причиной.

ts
// @ts-expect-error action(5); // Ok! // @ts-expect-error action('5'); // Error! Unused '@ts-expect-error' directive.

Проверка вызова функции в тернарном условном операторе

В версии 3.7 была добавлена проверка обязательного вызова функций участвующих в условном выражении if.

ts
declare function isValid(): boolean; function action(){ /** * Начиная с версии 3.7 * * [error] This condition will always return true * since the function is always defined. * Did you mean to call it instead? */ if(isValid){ /** * По факту этот блок кода будет * выполняться всегда, поскольку * в условном выражении участвует * ссылка на функцию, а не предполагаемый * результат её вызова! */ } }

Начиная с версии 3.9 подобное поведение было реализованно и для тернарного условного оператора.

ts
declare function isValid(): boolean; function action(){ /** * Начиная с версии 3.9 * * [error] This condition will always return true * since the function is always defined. * Did you mean to call it instead? */ return isValid ? 'true' : 'false'; }

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Изменение поведения для оператора Non-Null при совместном использовании с оператором опциональной цепочки

После того, как начиная с версии 3.7 был реализован оператор опциональной последовательности (.?), функционал определенный стандартом ESMAScript, многие обратили внимание на нелогичность его поведения при совместном использовании с таким оператором, как Not-Null\Not-Undefined.

ts
type T = { f0?: { f1?: any; } } function f(p?:T){ p?.f0!.f1; } f({});

Как известно, оператор опциональной последовательности предполагает предотвращение выполнения цепочки вызовов и поскольку в коде выше в функцию f передается объект лишенный хоть каких-то опциональных признаков типа T, то ошибки при обращении к полю f1 через нулевую ссылку ассоциированную с полем f0 не произойдет.

То есть предполагается, что подобный код после компиляции примет следующий вид -

js
function f(p){ /** * Обращение к f1 произойдет только в случае * существования параметра p и определения в * нем поля f0 ссылающегося на объект. *// p === null || p === void 0 ? void 0 : p.f0.f1; }

И это логично!

Но до текущей версии подобный код разворачивался таким образом, что приводило к ошибке во время выполнения.

js
function f(p){ /** * Обращение к f1 произойдет даже в случае * если параметр p и\или поле f1 отсутствует, *, что приведет к ошибке во время выполнения. * Кроме того, подобное поведение в корне противоречит * ожидаемому разработчиком поведению оператора * опциональной последовательности. */ (p === null || p === void 0 ? void 0 : p.f0).f1; }

Исходя из этого начиная с версии 3.9 поведение оператора Not-Null\Not-Undefined используемого совместно с оператором опциональной цепочки было изменено на ожидаемое. В случае необходимости получения поведения предшествующего текущей версии предлагается конкретизировать выражение с помощью фигурных скобок.

ts
type T = { f0?: { f1?: any; } } function f(p?:T){ /** * Указываем, что обращение к полю f1 * должно произойти независимо от результата * выражения в круглых скобках. * * После компиляции данный код примет подобный вид - * * (p === null || p === void 0 ? void 0 : p.f0).f1; */ (p?.f0)!.f1; } f({});

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Возникновение ошибки при наличии в строке закрывающей фигурной или угловатой скобки в файлах с расширением TSX

Спецификация JSX не допускает наличие закрывающих фигурных (}) и угловых (>) скобок в строках. Поэтому начиная с текущей версии при явном их указании будет возникать ошибка, текст которой предложит решение в виде их экранирования или замены на спец-символы допускаемые JSX спецификацией.

ts
let text = [ // Unexpected token. Did you mean `{'}'}` or `&rbrace;`? <span>Text with closing curly bracket }.</span>, // Unexpected token. Did you mean `{'>'}` or `&gt;`? <span>Text with closing angle bracket >.</span>, ];

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Повышение уровня проверки необязательных полей для типов определяющих тип пересечение

До версии 3.9 такой тип пересечения (Intersection) как A & B присваивается типу C если A или B присваивается C. При наличии в A или B необязательных членов это может привести к неожиданным последствиям.

ts
interface A { a: number; } interface B { b: string; } interface C { a?: boolean; b: string; } declare let x: A & B; declare let y: C; /** * Ok до версии 3.9, поскольку A можно присвоить C * и B можно присвоить C. */ y = x;

Поэтому начиная с текущей версии поведение изменено таким образом, что пока каждый тип определяющим пересечение является объектным типом, система типов будет рассматривать все члены сразу.

Поэтому в рассматриваемом коде возникнет следующая ошибка -

ts
// ...код /** * Error начиная с версии 3.9 - * * Type 'A & B' is not assignable to type 'C'. * Types of property 'a' are incompatible. * Type 'number' is not assignable to type 'boolean | undefined'. */ y = x;

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Определение типа пересечения дискриминантными полями

До версии 3.9 сужение на основе дискриминантных полей определяющих тип пересечение (Intersection) определяло такие поля, как принадлежащие к типу never.

ts
declare function join<T, U>(a: T, b: U): T & U; interface NameInfo { type: "name"; firstName: string; lastName: string; } interface AddressInfo { type: "address"; country:string; city: string; } declare let nameInfo: NameInfo; declare let addressInfo: AddressInfo; /** * let person: NameInfo & AddressInfo */ let person = join(nameInfo, addressInfo); /** * Ok, До версии 3.9 */ person.type; // (property) type: never

Поскольку на практике потеря информации о типах полей недопустима начиная с текущей версии вывод типов определит как тип never не дискриминантные поля, а сам тип пересечения.

ts
// ...код /** * let person: never */ let person = join(nameInfo, addressInfo); /** * Error, Начиная с версии 3.9 - * * Property 'type' does not exist on type 'never'. */ person.type; // (property) type: never

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Параметр типа расширяющий тип any больше не наследует его характеристики

Раньше параметр типа расширяющий тип any наделялся всеми его характеристиками, что при указании его в качестве типа снижало уровень типобезопасности программы.

ts
const f = <T extends any>(p: T) => { /** * [*] * До версии 3.9 - Ok * Начиная с версии 3.9 - Error -> * Property 'notExistsMethod' does not exist on type 'T'. */ p.notExistsMethod(); // [*] }

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] get и set больше не перечисляемы

До версии 3.9 при генерации кода для аксессоров определенных в теле класса под es5 \ es2015 поле enumerable устанавливалось в значение true, в, то время как спецификация ESMAScript предполагает false.

ts
class T { set accessor(value: string){ } get accessor(){ return "accessor"; } }
js
"use strict"; var T = /** @class */ (function () { function T() { } Object.defineProperty(T.prototype, "accessor", { get: function () { return "accessor"; }, set: function (value) { }, enumerable: false, // false начиная с версии 3.9, но true для версий ниже configurable: true }); return T; }());

Начиная с текущей версии расхождение со спецификацией ESMAScript было исправлено.

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] export * теперь всегда включается в сборку

Раньше реэкспорт вида export * from "path/to/module"; не включался в сборку, если модуль не экспортировал валидных с точки зрения JavaScript конструкций. Это поведение вставляло палки в колёса такому компилятору, как Babel из-за чего было принято решение изменить поведение.

Начиная с текущей версии подобные модули будут включены в конечную сборку.