beta

4.3

Отдельные типы аксеcсоров

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

js
class T {
    private _value = 0;

    /**
     * Предполагается, что геттер value возвращает тип number.
     */
    get value() {
        return this._value;
    }

    /**
     * Предполагается, что сеттер может обрабатывать
     * сценарии работы с любым типом.
     * 
     * [0] преобразовываем value к типу number. Это необходимо,
     * посколькуможет оно может принадлежать к любому типу.
     * 
     * [1] Если значение convertedValue не способно быть преобразовано,
     * то прекращаем выполнение кода сеттера...
     * 
     * [2] ...иначе, присваиваем его приватному полю _value.
     */
    set value(value){
        let convertedValue = Number(value); // [0]

        if(!Number.isFinite(convertedValue)){ // [1]
            return; // [2]
        }

        this._value = convertedValue; // [3]
    }
}

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

ts
// Ok

class T0 {
    private _value = 0;

    get value(): number {
        return this._value;
    }

    /**
     * [*] Ok
     */
    set value(value: number){ // [*]
    }
}

// Error

class T1 {
    private _value = 0;

    get value(): number {
        return this._value;
    }

    /**
     * [*] Error -> 'get' and 'set' accessor must have the same type.ts(2380)
     */
    set value(value: number | string | boolean){ // *
    }
}

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

ts
// Начиная с текущей версии..

class T0 {
    private _value = 0;

    get value(): number {
        return this._value;
    }

    /**
     * [*] Ok
     */
    set value(value: number | string | boolean){ // *
    }
}

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

ts
class T {
    private _value = 0;

    /**
     * [*] Error -> The return type of a 'get' accessor must be assignable to its 'set' accessor typets(2380)
     */
    get value(): number { // [*]
        return this._value;
    }

    /**
     * Сеттер не включает тип возвращаемый геттером.
     */
    set value(value: string | boolean){
    }
}

Модификатор override и флаг --noImplicitOverride

Представьте случай переопределения подклассом некоторых методов своего суперкласса.

ts
class SuperClass {
    /**
     * [*] Определяет метод
     */
    a(){} // [*]
    b(){}// [*]
}
class SubClass extends SuperClass {
    /**
     * [*] Переопределяет методы своего суперкласса.
     */
    a(){} // [*]
    b(){} // [*]
}

Но что, если над проектом работает большое количество команд находящихся в разных уголках земного шара и вдруг, разработчики SuperClass, решили изменить его api удалив оба метода? В таком случае, разработчики класса SubClass даже не узнают об этом, поскольку переопределение превратится в определение. Другими словами, компилятор даже глазом не моргнет, поскольку ему будет казаться, что класс SubClass определят методы a() и b().

ts
class SuperClass {
    /**
     * Удалили a() и b() и добавили c().
     */
    с(){}
}
class SubClass extends SuperClass {
    /**
     * Ошибки не возникает, так как компилятор считает
     * что данный класс определяет оба метода.
     */
    a(){}
    b(){}
}

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

ts
class SuperClass {
    /**
     * Удалили a() и b() и добавили c().
     */
    с(){}
}
class SubClass extends SuperClass {
    /**
     * [*] Error -> 
     * This member cannot have an 'override' modifier
     * because it is not declared in the base class 'SuperClass'.ts(4113)
     * 
     * Теперь компилятор понимает, что происходи переопределение
     * несуществующих методов.
     */
    override a(){} // [*]
    override b(){} // [*]
}

А во избежание затираний возникающих в случаях определения суперклассом методов уже определенных в его потомках, был введен новый флаг компилятора --noImplicitOverride.

ts
/**
 * [0] метод определенный только в SubClass
 */
class SuperClass {
    a(){}
}
class SubClass extends SuperClass {
    b(){} // [0]
}

/**
 * [1] Но спустя некоторое время класс SuperClass
 * определяет метод b(), который уже существует в
 * классе-потомке [2]. Другими словами, произошло
 * нежелаемое переопределение способное привести
 * к непредсказуемому поведению программы.
 */
class SuperClass {
    a(){}
    b(){} // [1]
}
class SubClass extends SuperClass {
    b(){} // [2]
}

При активации флага --noImplicitOverride, в подобных случаях будет возникать ошибка.

ts
class SuperClass {
    a(){}
    b(){}
}
class SubClass extends SuperClass {
    /**
     * --noImplicitOverride = true
     * 
     * [*] Error -> This member must have an 'override'
     * modifier because it overrides a member in the base
     * class 'SuperClass'.ts(4114)
     */
    b(){} // [*]
}

Улучшение работы шаблонного строкового типа

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

ts
type SeaFish = `shark` | `barracuda`;
type RiverFish = `pike` | `pike perch`;
type FishSoup = `${SeaFish | RiverFish} fish`;

// type FishSoup = "shark fish" | "barracuda fish" | "pike fish" | "pike perch fish"

Кроме того, при установлении совместимости компилятор в состоянии определить совместимость обычных типов, как например number или boolean с их литеральными представителями.

ts
declare let v0: `${number}-${number}-${number}`;
declare let v1: `1-2-3`;

v0 = v1; // Ok

Тем не менее, компилятору не под силу вычисления на основе типа значения.

ts
/**
 * [*] Error -> Type 'string' is not assignable to type '`Hello ${string}`'.ts(2322)
 * Хотя компилятор знает тип значения param, он не может расспознать совместимость
 * типа возвращаемого значения с типом указанным в сигнатере функции.
 */
function f(param: string): `Hello ${string}` {
    return `Hello ${param}`; // [*]
}

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

ts
/**
 * >=v4.3
 * 
 * [*] Ok!
 */
function f(param: string): `Hello ${string}` {
    return `Hello ${param}`; // [*]
}

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

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

ts
/**
 * Фабрика возвращающая объект обещания.
 */
function factory(){
    return Promise.resolve(false);
}

/**
 * Действие, логика которого зависит от существования
 * объекта Promise.
 */
async function action() {
    /**
     * Если объект обещания существует, то выполняем 
     * некоторые действия. 
     */
    if(factory()){
        // ...
    }
}

Но что, если по условию, в условном выражении требуется проверить не существование объекта Promise, а результат его выполнения. К тому, обещание является возвращаемым значением асинхронной функции являющейся асинхронным валидатором. Если разработчик упустит из внимания факт асинхронности валидатора, то в приложении появится трудновыявляемая ошибка, поскольку без ключевого слова await в условном выражении проверяться будет не результат разрешения обещания, а существование его объекта.

ts
/**
 * [0] Асинхронный валидатор
 */

async function validate(): Promise<boolean> { // [0]
    return false;
}


/**
 * [1] Асинхронное действие результат выполнения которого
 * зависит от асинхронного валидатора.[2] Но несмотря на то, что
 * результатом выполнения функции validate() является значение flase,
 * выполнение программы все равно зайдет в блок if поскольку
 * валидатор, по невнимательности, был вызван без ключевого слова await.
 * 
 */ 
async function action(): Promise<void> {
    /**
     * --strictNullChecks = true
     * 
     * До текущей версии - Ok, поскольку компилятор проверяет существоание объекта Promise.
     * Начиная с текущей версии - Error.
     * 
     * This condition will always return true since this 'Promise<boolean>' appears to always be defined.ts(2801)
     */
    if (validate()) { // [2]
    }
}

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

ts
async function validate(): Promise<boolean> { // [0]
    return false;
}
async function action(): Promise<void> {
    /**
     * --strictNullChecks = true
     */
    if (validate()) { // Error
    }
    if (validate() !== null) { // Ok
    }


    validate() ? true : false; // Error
    validate() !== null ? true : false; // Ok

    validate() && true; // Error
    validate() !== null && true; // Ok

}

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

ts
    return false;
}
async function action(): Promise<void> {
    /**
     * --strictNullChecks = true
     */
    if (await validate()) { // Ok
    }

    await validate() ? true : false; // Ok

    await validate() && true; // Ok
    await validate() || true; // Ok

}

Единственное о чем ещё стоет упомянуть, что на данный момент, по неизвестной причине, в условном выражении ИЛИ (||), при отсутствии ключевого слова await ошибка не возникает.

ts
validate() || true; // Ok
validate() !== null || true; // Ok

Индексные сигнатуры класса

До этого момента индексные сигнатуры являлись исключительно членами экземпляра класса.

ts
class T {
    /**
     * Инжексная сигнатура принадлежащая
     * экземпляру класса.
     */
    [key: string]: string;
}

let instance = new T();
instance['value'] = '🍨'; // динамическое объявление 

Начиная с текущей версии индексные сигнатуры также можно определять на уровне самого класса (static).

ts
class T {
    /**
     * Индексная сигнатура принадлежащая
     * классу.
     */
    static [key: string]: string;
}

T['value'] = '🍨'; // динамическое объявление 

Правила для статических индексных сигнатур аналогичны правилам индексных сигнатур экземпляра класса.

Расширение возможностей ECMAScript приватного модификатора и флаг --useDefineForClassFields

До текущего момента TypeScript позволял применять нативный ECMAScript модификатор доступа private (#) только к полям экземпляра.

ts
/**
 * >=v4.3
 * 
 * [*] Error!
 */
class T {
    /** члены класса */

    static #CLASS_FIELD = ""; // [*]
    static get #classProp(){ // [*]
        return T.#CLASS_FIELD;
    }
    static set #classProp(value: string){ // [*]
    }
    static #classMethod(){ // [*]

    }


    /** члены экземпляра класса */

    #instanceField = "";

    get #instanceProp(){ // [*]
        return this.#instanceField;
    }
    set #instanceProp(value: string){ // [*]
    }

    #instanceMethod(){ // [*]

    }
}

Начиная с текущей версии, данный модификатор можно применить ко всем членам, как экземпляра, так и класса. Единственный неочевидный момент связан с полями класса, поскольку их модифицирование возможно только при активации нового флага --useDefineForClassFields.

ts
/**
 * >=v4.3
 * 
 * Ok! Но с одной оговоркой. [*] Для
 * разрешения нативных приватных полей класса
 * следует активировать флаг --useDefineForClassFields,
 * иначе возникнет ошибка.
 */
class T {
    /** члены класса */

    static #CLASS_FIELD = ""; // [*]
    static get #classProp(){
        return T.#CLASS_FIELD;
    }
    static set #classProp(value: string){
    }
    static #classMethod(){

    }


    /** члены экземпляра класса */

    #instanceField = "";

    get #instanceProp(){
        return this.#instanceField;
    }
    set #instanceProp(value: string){
    }

    #instanceMethod(){

    }
}

КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ Изменения в lib.d.ts

Как и всегда, во имя улучшений, основная библиотека lib.d.ts претерпела множество изменений, большая часть которых пришлась на удаление api Mozila не реализованное ни в одном браузере.

КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ Enum больше нельзя сравнивать с произвольными числами

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

ts
/**
 * До текущей версии.
 */

enum E {
    A = 0,
    B = 1
}

function f(p: E){
    /**
     * [0][1] Ok!
     * [2] - Несуществующий индекс.
     */
    if(p === 1){ // [0]

    }
    
    if(p === 2){ // [1]

    }
}

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

ts
/**
 * >=v4.3
 */

enum E {
    A = 0,
    B = 1
}

function f(p: E){
    /**
     * [0] Ok!
     * [1] Error ->
     * This condition will always return 'false' since the types 'E' and '2' have no overlap.ts(2367)
     */
    if(p === 1){ // [0]

    }
    
    if(p === 2){ // [1]

    }
}