3.9
Улучшение вывода типа для Promise.all
В последних версиях TypeScript (начиная с версии 3.7
) были обновлены декларации для таких методов как Promise.all
и Promise.race
. Но к сожалению это привело к неожиданным результатам в работе вывода типа, что более всего стало очевидно если в выводе участвуют null
или undefined
.
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
оно было исправлено должным образом.
// ... 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
в некоторых моментах может возникать разногласия.
Представьте ситуацию при которой необходимо покрыть тестами функцию принимающую на вход строковой параметр и кроме того выполняющей в своем теле его валидацию времени выполнения.
function isStringAssert(valid:boolean): asserts valid { if(!valid){ throw new Error(`...`); } } function action(value: string){ isStringAssert(typeof value === "string"); // валидация времени выполнения // некоторый код... }
Поскольку лучшие практики тестирования предполагают написание таких тестов которые по своей природе не должны пройти, тестировщик пишущий тесты также на TypeScript
при попытке протестировать ошибку время выполнения (assert
) столкнется с проблемой время компиляции, так как компилятор не позволит скомпилировать код выявив несоответствие типов.
// файл .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-expect-error action(5); // Ok! // @ts-expect-error action('5'); // Error! Unused '@ts-expect-error' directive.
Проверка вызова функции в тернарном условном операторе
В версии 3.7
была добавлена проверка обязательного вызова функций участвующих в условном выражении if
.
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
подобное поведение было реализованно и для тернарного условного оператора.
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
.
type T = { f0?: { f1?: any; } } function f(p?:T){ p?.f0!.f1; } f({});
Как известно, оператор опциональной последовательности предполагает предотвращение выполнения цепочки вызовов и поскольку в коде выше в функцию f
передается объект лишенный хоть каких-то опциональных признаков типа T
, то ошибки при обращении к полю f1
через нулевую ссылку ассоциированную с полем f0
не произойдет.
То есть предполагается, что подобный код после компиляции примет следующий вид -
function f(p){ /** * Обращение к f1 произойдет только в случае * существования параметра p и определения в * нем поля f0 ссылающегося на объект. *// p === null || p === void 0 ? void 0 : p.f0.f1; }
И это логично!
Но до текущей версии подобный код разворачивался таким образом, что приводило к ошибке во время выполнения.
function f(p){ /** * Обращение к f1 произойдет даже в случае * если параметр p и\или поле f1 отсутствует, *, что приведет к ошибке во время выполнения. * Кроме того, подобное поведение в корне противоречит * ожидаемому разработчиком поведению оператора * опциональной последовательности. */ (p === null || p === void 0 ? void 0 : p.f0).f1; }
Исходя из этого начиная с версии 3.9
поведение оператора Not-Null\Not-Undefined
используемого совместно с оператором опциональной цепочки было изменено на ожидаемое. В случае необходимости получения поведения предшествующего текущей версии предлагается конкретизировать выражение с помощью фигурных скобок.
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
спецификацией.
let text = [ // Unexpected token. Did you mean `{'}'}` or `}`? <span>Text with closing curly bracket }.</span>, // Unexpected token. Did you mean `{'>'}` or `>`? <span>Text with closing angle bracket >.</span>, ];
[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Повышение уровня проверки необязательных полей для типов определяющих тип пересечение
До версии 3.9
такой тип пересечения (Intersection
) как A & B
присваивается типу C
если A
или B
присваивается C
. При наличии в A
или B
необязательных членов это может привести к неожиданным последствиям.
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;
Поэтому начиная с текущей версии поведение изменено таким образом, что пока каждый тип определяющим пересечение является объектным типом, система типов будет рассматривать все члены сразу.
Поэтому в рассматриваемом коде возникнет следующая ошибка -
// ...код /** * 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
.
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
не дискриминантные поля, а сам тип пересечения.
// ...код /** * 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
наделялся всеми его характеристиками, что при указании его в качестве типа снижало уровень типобезопасности программы.
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
.
class T { set accessor(value: string){ } get accessor(){ return "accessor"; } }
"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 из-за чего было принято решение изменить поведение.
Начиная с текущей версии подобные модули будут включены в конечную сборку.