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

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