release

4.2

Изменение поведения указания остаточных типов в кортеже

Начиная с текущей версии вводятся ограничения на указание в кортеже остаточных типов (...rest). Теперь, в случае если перед остаточным типом указан другой остаточный тип или после него следует необязательный тип ([number?]), возникнет ошибка.

ts
/**
 * [0] A rest element cannot follow another rest element.ts(1265)
 *
 */
let v0: [...boolean[], ...string[]]; // Error [0]
let v1: [...boolean[], boolean, ...string[]]; // Error [0]

let v2: [...boolean[], number]; // Ok
let v3: [number, ...boolean[]]; // Ok
let v4: [number, ...boolean[], number]; // Ok

/**
 * [0] An optional element cannot follow a rest element.ts(1266)
 *
 */
let v5: [...boolean[], boolean?]; // Error [1]

let v6: [boolean?, ...boolean[]]; // Ok

Изменение вывода для псевдонимов типов

До текущей версии, псевдонимы типов, в разных ситуациях, выводились по-разному. Если тип значения представлялся псевдонимом типа, то таковым оно и определялось при выводе информации в консоль или подсказках ide. Но когда псевдоним типа участвует при определении другого типа, то выводится типом(и) для которого он был назначен, то есть разворачивается.

ts
type ValueType = number | string | boolean;


/**
 * Тип параметра value выводится как псевдоним типа,
 * который в выводе типа возвращаемого значения разворачивается. 
 * function action(value: ValueType): string | number | boolean | undefined
 * 
 */
function action(value: ValueType){
    if(Math.random() < 0.5){
        return undefined;
    }
    return value;
}


/**
 * Подобное поведение аналогично и для любых
 * других похожих случаев.
 * 
 * let v0: ValueType
 * let v1: string | number | boolean | undefined
 */
declare let v0: ValueType;
let v1 = Math.random() < 0.5 ? v0 : undefined;

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

ts
type ValueType = number | string | boolean;


/**
 * Псевдоним типа не разворачивается
 * function action(value: ValueType): ValueType | undefined
 * 
 */
function action(value: ValueType){
    if(Math.random() < 0.5){
        return undefined;
    }
    return value;
}


/**
 * Подобное поведение аналогично и для любых
 * других похожих случаев.
 * 
 * let v0: ValueType
 * let v1: ValueType | undefined
 */
declare let v0: ValueType;
let v1 = Math.random() < 0.5 ? v0 : undefined;

Ужесточение правил проверки для in-оператора

В JavaScript при использовании необъектного типа в правой части выражения с оператором in возникнет ошибка времени выполнения. Начиная с текущей версии, TypeScript умеет распознавать данную ошибку во время компиляции.

ts
"hasOwnProperty" in {}; // Ok
"hasOwnProperty" in 0; // Error -> The right-hand side of an 'in' expression must not be a primitive.ts(2361)

Новый флаг --noPropertyAccessFromIndexSignature

После того, как несколько версий назад в TypeScript было разрешено обращаться к динамическим полям объекта не только при помощи скобочной (o['field']), но и точечной нотации (o.field), стала очевидна проблема связанная с необязательными полями определенных в том же объекте. Другими словами, потерян механизм отделения предопределенных полей, от динамических.

ts
type Settings = {
    env?: string[]; // определение необязательного предопределенного поля

    [key: string]: any; // определение динамических полей
}


function configurate(settings: Settings){
    /**
     * [0] хотя поле env является предопределенным,
     * ошибки не возникает, поскольку компилятор предполагает,
     *, что обращение происходит к динамическому свойству envs.
     *
     * Отсюда выходит, что нет способа защитится от ошибки.
     *
     * [1] обращение к динамическому свойству.
     * [2] во всех случаях допустимо обращение,
     * как через скобочную, так и точечную нотацию.
     */
    //----------------------------
    if(settings.envs){ // Ok [0]

    }
    if(settings['envs']){ // Ok [0]

    }

    //----------------------------
    if(settings.env){ // Ok [0]

    }
    if(settings['env']){ // Ok [0]

    }
}

Для исправления этого был добавлен флаг --noPropertyAccessFromIndexSignature, активация которого возвращает старое поведение. То есть, обращение с помощью точечной нотации допускается только для предопределённых полей.

ts
type Settings = {
    env?: string[]; // определение необязательного предопределенного поля

    [key: string]: any; // определение динамических полей
}


function configurate(settings: Settings){
    /**
     * [0] Property 'envs' comes from an index signature, so it must be accessed with ['envs'].ts(4111)
     *
     *
     * [1] обращение к предопределенному свойству.
     * [2] при помощи скобочной нотации можно обращаться ко всем полям.
     * Точечная нотация только для предопределенных полей.
     */
    //----------------------------
    if(settings.envs){ // Error [0]

    }
    if(settings['envs']){ // Ok [0]

    }

    //----------------------------
    if(settings.env){ // Ok [1]

    }
    if(settings['env']){ // Ok [1]

    }
}

Модификатор abstract для описания типа конструктора

Абстрактные классы предназначены исключительно для расширения (невозможно создать его экземпляр с помощью оператора new), а его абстрактные члены должны обязательно должны быть переопределены потомками.

ts
/**
 * Абстрактный класс с одним абстрактным методом.
 */
abstract class Shape {
    abstract getRectangle(): ClientRect;
}

/**
 * Из-за того, что класс абстрактный не получится создать его экземпляр с помощью оператора new.
 */
new Shape(); // Error -> Cannot create an instance of an abstract class.ts(2511)


/**
 * [0] Кроме этого, подкласс обязательно должен переопределять абстрактные члены своего суперкласса.
 */
class Circle extends Shape {
    getRectangle(){// [0]
        return {
            width:0,
            height: 0,
            top: 0,
            right: 0,
            bottom: 0,
            left: 0
        };
    }
}

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

ts
interface IHasRectangle {
    getRectangle(): ClientRect;
}

type HasRectangleClass = new() => IHasRectangle;

/**
 * [*] Type 'typeof Shape' is not assignable to type 'HasRectangleClass'.
 Cannot assign an abstract constructor type to a non-abstract constructor type.ts(2322)
 */
let ClassType: HasRectangleClass = Shape; // Error [*]

Кроме этого, невозможно получить тип экземпляра абстрактного класса с помощью вспомогательного типа InstanceType<T>.

ts
/**
 * [*] Type 'typeof Shape' does not satisfy the constraint 'new (...args: any) => any'.
 Cannot assign an abstract constructor type to a non-abstract constructor type.ts(2344)
 */
type Instance = InstanceType<typeof Shape>; // Error [*]

Это в свою очередь не позволяет реализовать механизм динамического наследования.

ts
function subclassCreator(Base: new() => IHasRectangle){
    return class extends Base {
        getRectangle(){
            return {
                width:0,
                height: 0,
                top: 0,
                right: 0,
                bottom: 0,
                left: 0
            };
        }
    }
}

/**
 * [*] Argument of type 'typeof Shape' is not assignable to parameter of type 'new () => IHasRectangle'.
 Cannot assign an abstract constructor type to a non-abstract constructor type.ts(2345)
 */
subclassCreator(Shape); // Error [*] -> передача в качестве аргумента абстрактный класс
subclassCreator(Circle); // Ok -> передача в качестве аргумента конкретный класс

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

ts
interface IHasRectangle {
    getRectangle(): ClientRect;
}

type HasRectangleClass = abstract new() => IHasRectangle;


let ClassType: HasRectangleClass = Shape; // Ok

Несмотря на то, что тип класса имеет абстрактный модификатор, он также остается совместимым с типами конкретных классов.

ts
function subclassCreator(Base: abstract new() => IHasRectangle){
    return class extends Base {
        getRectangle(){
            return {
                width:0,
                height: 0,
                top: 0,
                right: 0,
                bottom: 0,
                left: 0
            };
        }
    }
}

subclassCreator(Shape); // Ok -> абстрактный класс
subclassCreator(Circle); // Ok -> конкретный класс

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

ts
type AbstractInstanceType<T extends abstract new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

type Instance = AbstractInstanceType<typeof Shape>; // Ok

Понимание структуры проекта с флагом --explainFiles

У TypeScript разработчиков часто возникает вопрос, почему в сборку был включён тот или иной файл, даже если он был добавлен в exclude. Поэтому, для лучшего понимания работы компилятора tsc был добавлен новый флаг --explainFiles позволяющий выводить информацию о зависимостях.

ts
tsc --explainFiles

Помимо вывода в консоль, данный флаг позволяет записывать информацию в файл, или даже открывать в visual studio code.

bash
// вывод в файл
tsc --explainFiles > explanation.txt
    
// вывод в редактор vsc
tsc --explainFiles | code -

Проверка вызова функций в логических выражениях

Теперь, проверка вызова функций доступна и для логических выражений с применением операторов && и ||.

ts
function shouldDisplayElement(element: Element) {
    return true;
}

function getVisibleItems(elements: Element[]) {
    /**
     * [*] Error -> This condition will always return true since the function is always defined.
     * Did you mean to call it instead?
     */
    return elements.filter(e => shouldDisplayElement /**[*] */ && e.children.length);
}

Смягчение правил между необязательными членами и строковых индексных сигнатур

В предыдущих версиях, совместимость объектного типа, определяющего обязательную строковую индексную сигнатуру, с объектным типом предполагающем необязательные члены, были не совместимы.

ts
/**
 * Версия <4.2
 */
type Magic = {
    fire?: string[];
    water?: string[];
}

declare const HERO_CONFIG: Magic;

/**
 * [*] Type 'Magic' is not assignable to type '{ [key: string]: string[]; }'.
 *
 * Ошибка возникает потому, что тип переменной hero
 * предполагает обязательные свойства, в, то время, как объект
 * HERO_CONFIG - необязательные, а значит и возможно отсутствующие.
 *
 */
const hero: {[key: string]: string[]} = HERO_CONFIG;
/**[*]*/

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

ts
/**
 * Версия >=4.2
 */
type Magic = {
    fire?: string[];
    water?: string[];
}

declare const HERO_CONFIG: Magic;

const hero: {[key: string]: string[]} = HERO_CONFIG;
/**Ok*/

Единственное стоит уточнить, что ошибка по-прежнему возникает при попытке инициализировать необязательные поля значением undefined...

ts
/**
 * Версия >=4.2
 * 
 * [0] поля определены как обязательные!
 */
type Magic = {
    fire?: string[];
    water?: string[];
}

declare const HERO_CONFIG: Magic;

/**
 * [1] Error -> 
 * Type 'undefined' is not assignable to type 'string[]'.
 */
const hero: {[key: string]: string[]} = HERO_CONFIG;
hero['fire'] = ['fireball']; // Ok
hero['water'] = undefined; // Error [1]

...а также тогда, когда объектный тип, вместо опционального оператора, явно указывает принадлежность типа членов к типу undefined.

ts
/**
 * Версия >=4.2
 *
 * [0] поля определены как обязательные!
 */
type Magic = {
    fire: string[] | undefined; // [0]
    water: string[] | undefined; // [0]
}

declare const HERO_CONFIG: Magic;

/**
 * [1] Error ->
 * Type 'Magic' is not assignable to type '{ [key: string]: string[]; }'.
 */
const hero: {[key: string]: string[]} = HERO_CONFIG;
/**[1]*/

И кроме этого, данные правила применимы исключительно к строковой индексной сигнатуре.

ts
/**
 * Версия >=4.2
 * 
 * [0] ключи полей определены как индексы!
 */
type Port = {
    80?: string; // [0]
    88?: string; // [0]
}

declare const SERVER_PORT: Port;

/**
 * [2] Ключ индексной сигнатуры принадлежит к типу number.
 * 
 * [1] Error -> 
 * Type 'Port' is not assignable to type '{ [key: number]: string[]; }'.
 */
const port: {[key: number]: string[]} = SERVER_PORT;
   /**[1] */     /**[2] */

Деструктурированные переменные можно явно пометить как неиспользуемые

До этого момента, при активном флаге --noUnusedLocals, неиспользуемая переменная объявленная в процессе деструктуризации вызывала ошибку.

ts
function f(){
    /**
     * [*] Error -> 'first' is declared but its value is never read.
     */
    let [first /** [*] */, second] = [0, 1];

    return second;
}

Начиная с текущей версии, для подобных ситуаций была создана возможность объявлять переменные, как неиспользуемые. Переменная объявленная в процессе деструктуризации считается необязательной, если её идентификатор начинается с нижнего подчёркивания _identifier.

ts
function f(){
    let [_first /** Ok */, second] = [0, 1];

    return second;
}

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Обновление lib.d.ts

Библиотека lib.d.ts претерпела множество изменений, самые значительные из которых коснулись таких типов, как Intl и ResizeObserver.

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] noImplicitAny и новое поведение для оператора yield тип которого не может быть установлен явно

Начиная с текущей версии, при активном флаге --noImplicitAny, поведение оператора yield, тип значения которого не может быть установлен контекстуально, было изменено. Теперь, если тип значения не может быть выявлен явно, возникает ошибка.

ts
function* g0 () {
    /**
     * Версия <4.2 -> Ok
     * Версия >=4.2 -> Error
     * 'yield' expression implicitly results in an 'any' type because its containing generator lacks a return-type annotation.
     */
    const value = yield 1; // Error
}

function* g1 () {
    /**
     * Версия <4.2 -> Ok
     * Версия >=4.2 -> Ok
     * Потому, что значение не используется.
     */
    yield 1;
}

function* g2 () {
    /**
     * Версия <4.2 -> Ok
     * Версия >=4.2 -> Ok
     * Потому, что явно задан тип string.
     */
    const value: string = yield 1;
}


function* g3 (): Generator<number, void, string> {
    /**
     * Версия <4.2 -> Ok
     * Версия >=4.2 -> Ok
     * Потому, что тип указан в возвращаемом типе.
     */
    const value = yield 1;
}

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Проверка вызова функций в условных операторах приурочена к флагу --strictNullChecks

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

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Лимит для механизма spread в кортежах

Поскольку механизм распространения (spread) в кортежах может привести к зависанию компилятора, было принято решение, установить ограничение глубины в значение 10.000.

ts
const tuple = [0, '', true] as const; // инициализация кортежа с тремя значениями
const doubleTuple = [...tuple, ...tuple]; // инициализация кортежа с помощью механизма распространения, который приведет к удваиванию значений

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Изменение правил для оператора in

Изменение поведения оператора in, о котором было рассказано ранее, могут привести к ошибкам в существующем коде.

ts
"hasOwnProperty" in {}; // Ok
"hasOwnProperty" in 0; // Error -> The right-hand side of an 'in' expression must not be a primitive.ts(2361)

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Расширение .d.ts теперь нельзя использовать в путях импорта

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

ts
import {IAction} from "./types.d.ts"; // Error

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Изменение типа параметра функции обратного вызова передаваемой в качестве аргумента функции visitNode

В TypeScript есть функция visitNode, сигнатура которой определяет параметр функцию обратного вызова lift, тип параметра которой, теперь, вместо NodeArray<Node>, ожидает readonly Node[].

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Отмена поведения литерального вывода шаблона из версии 4.2@beta

В версии 4.2@beta было предложено поведение литерального вывода шаблона, которое в релизе было отменено.