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 из-за чего было принято решение изменить поведение.
Начиная с текущей версии подобные модули будут включены в конечную сборку.