release

3.7

Оператор опциональной последовательности (?.)

Начиная с текущей (v3.7) версии, TypeScript реализовал функционал обозначаемый как опертор опциональной последовательности (optional chaining operator) внесенный в спецификацию ECMScript комитетом TC39. Оператор опциональной последовательности обозначается вопросительным знаком после которого следует точка ?. и предназначен для безопасного обращения к членам объекта через ссылку которая может иметь значение null или undefined. Этого функционала очень давно все ждали, поэтому не будем медлить и немедля приступим к его рассмотрению на простом пример.

ts
/**
 * Вложенные друг в друга типы
 * (как матрешки) объявленные
 * семантически в обратном порядке.
 */
interface D {
    n: number;
}
interface C {
    d: D;
}
interface B {
    c: C;
}
interface A {
    b: B;
}

/**
 * Представьте сценарий по которому
 * ответ от сервера может представлять
 * из себя json соответствующий как типу
 * A так и объекту у которого отсутствуют
 * какие-либо принаки {}.
 */

let json = '{}';
let a: A = JSON.parse(json);

/**
 * При поппытке обращения к несуществующим
 * полям объекта возникнет соответствующее
 * исключение.
 */
let b = a.b; // Ok! поскольку отсутствуют операции над значением undefined
let c = a.b.c; // Runtime Error!
let d = a.b.c.d; // Runtime Error!
let n = a.b.c.d.n; // Runtime Error!

/**
 * Подобное можно избежать произведя
 * проверку на существование ссылок.
 */

if (a.b && a.b.c && a.b.c.d) {
    /**
     * Здесь можно обратится к полю n,
     * которое также может быть не определенно
     * что при попытке вызвать методы реализованные
     * в типе Number также приведет к исключению.
     * Чтобы этого избежать потребуется дополнительная
     * проверка. Но стоит сразу заметить, что обычной
     * проверки на существование поля a.b.c.d.n может
     * быть недостаточно, поскольку значение поля n може
     * быть 0, что при преобразовании типов преобразуется
     * в false. Поэтому помимо наличие самого поля необходимо
     * также проверить его значение.
     */

    if (a.b.c.d.n && !Number.isNaN(a.b.c.d.n)) {
        let r = a.b.c.d.n.toFixed(2);
    }
}

Сложно представить разработчика, который на практике не сталкивался с чем-то подобным и не испытывал желания избавится от написания утомительных проверок. И наконец свершилось! Разработчики TypeScript реализовали оператор опциональной последовательности известный также как элвис-оператор.

Элвис-оператор позволяет избавится от написания утомительных условных инструкций требуя от разработчика своего указания лишь в потенциально опасных местах. Оценить его мощь будет проще переписав предыдущий пример.

ts
interface D {
    n: number;
}
interface C {
    d: D;
}
interface B {
    c: C;
}
interface A {
    b: B;
}

let json = '{}';
let a: A = JSON.parse(json);

/**
 * Ещё раз стоит обратить внимание на то,
 * что указание элвис-оператора требуется
 * во всех потенциально опасных местах, поскольку
 * наличие одной ссылки не гарантирует наличие остальных
 * во всей цепочки выовов.
 *
 * 0) поскольку отсутствуют операции над значением undefined
 * 1) если ссылка на "b" существует вернуть значение ассоциированное с полем "c"
 * 2) если ссылки на "b" и "c" существуют вернуть значение ассоциированное с полем "d"
 * 3) если ссылки на "b" и "c" и "d" существуют вернуть значение ассоциированное с полем "n"
 * 4) если ссылки на "b" и "c" и "d" и "n" существуют вернуть значение возвращенное методом "toFixed"
 */
let b = a.b; // Ok! (0)
let c = a.b?.c; // Ok! (1)
let d = a.b?.c?.d; // Ok! (2)
let n = a.b?.c?.d?.n; // Ok! (3)
let r = a.b?.c?.d?.n?.toFixed(2); // Ok! (4)

В случаях, когда элвис-оператор обнаруживает ссылку со значением null или undefined, результатом возвращенным из выражения становится undefined.

ts
interface IT {
    a: {
        n: number;
    };
}

let o0: IT = JSON.parse('{}');
let o1: IT = JSON.parse('{a: null}');

/**
 * Несмотря на то, что во втором случае
 * значение поля "a" равно null, n1,
 * также как и n0 будет иметь значение
 * и следовательно тип undefined.
 */
let n0 = o0?.a.n; // n0 имеет значение undefined;
let n1 = o0?.a.n; // n1 имеет значение;

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

ts
interface IT {
    a: {
        n: number;
    };
}

let o0: IT = JSON.parse('{}');
let o1: IT = JSON.parse('{a: null}');

let n0 = o0?.a.n; // let n0: number; а не number | undefined
let n1 = o0?.a.n; // let n1: number; а не number | undefined

Кроме того элвис-опрератор также может быть применен при вызове методов, а в случае отстутствия ссылок на них результатом вызова также будет undefined.

ts
interface IT {
    f: (() => number) | null;
}
class T implements IT {
    /**
     * Вводим вывод типов в амешательство путем
     * присваивания функции в положительном случае
     * и null в отрицательном.
     */
    f = Math.round(Math.random() * 1) === 1 ? () => 10 : null;
}

let t = new T();
let n = t.f?.(); // результатом выражения вызова метода является undefined

Таким образом выход версии v3.7 дал разработчикам на языке TypeScript инструмент предотвращающий исключения связанные с обращением к отсутствующим ссылкам или ссылкам имеющим значение null, который кроме всего не чувствителен к значениям преобразование которых к типу Boolean принимает ложную форму (0, NaN, false), что в свою очередь ознаминовало начало эпохи в которой нет места конструкциям выполняющих утомительные и проверки наличия ссылок.

Оператор объединения со значением null (??)

Ко всему прочему начиная с версии v3.7 в TypeScript был реализован механизм обозначеный в спецификации ECMScript как объединение со значение null (nullish coalescing) для чего в синтаксис был введен новый оператор представленный двумя знаками вопроса ?? по обе стороны которого распологаются опернады left-operand ?? right-operand. В случае когда операнд расположенный левее оператора имеет значение null или undefined то результатом выражения является операнд находящийся правее оператора. Это очень похоже на работу логического оператора или (||) за исключением того, что последний взаимодействует с любыми значениями, в то время как новый оператор исключительно с null и undefined, что в некоторых случаях избавляет от дополнительных условий.

ts
null || 'default'; // default
undefined || 'default'; // default
false || 'default'; // default
0 || 'default'; // default
NaN || 'default'; // default
'' || 'default'; // default

null ?? 'default'; // default
undefined ?? 'default'; // default
false ?? 'default'; // false
0 ?? 'default'; // 0
NaN ?? 'default'; // NaN
'' ?? 'default'; // ''
```

Механизм _объединение со значением null_ является прекрасным дополнением другого такого механизма, как _опциональная последовательность_. В то время как второй механизм предотвращает исключения при операциях над ссылками имеющими значение `null` или отсутствующими вовсе `undefined`, первый предоставляет возможность задасть значение по умолчанию только при реальном его отсутствии.

`````ts
interface A {
    b: {
        c: {
            n: number;
        };
    };
}

let a: A = JSON.parse('{}');
let n = a?.b?.c?.n ?? 0; // let n: number = 0;
```

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

Утверждение в сигнатуре (Signature Assertion)

Во многих языках, в том числе и Node.js, реализован функционал обозначаемый как assert и представленный функциями принимающими условие, в случае ложности которого выбрасывается исключение.

ts
import assert, { AssertionError } from 'assert';

try {
    assert(5 === Math.round(Math.random() * 5));
} catch (error) {
    console.log(error instanceof AssertionError); // true
}

До версии ёё v3.7 полноценно реализовать подобный механизм было невозможно. Поэтому начиная с текущей версии, язык TypeScript пополнился новой концепцией обозначаемой как утверждение в сигнатуре (assertion signatures) с помощью которых стало возможным моделирование рассмотренного выше приведения поведения.

ts
import { AssertionError } from 'assert';

/**custom assert */
const DEFAULT_ASSERTION_MESSAGE = 'this condition is false';
function stringAssert(condition: any, message?: string): asserts condition {
    if (!condition) {
        throw new AssertionError({
            message: message ?? DEFAULT_ASSERTION_MESSAGE,
        });
    }
}

const toUpperCase = (text: any) => {
    text.touppercase(); // not error

    stringAssert(typeof text === 'string');

    // text.touppercase(); // error

    return text.toUpperCase();
};

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

ts
function isStringAssert(value: any): asserts value is string {
    if (typeof value !== 'string') {
        throw new Error(`value is not type string`);
    }
}
const toUpperCase = (text: any) => {
    text.touppercase(); // not error

    isStringAssert(text);

    // text.touppercase(); // error

    return text.toUpperCase();
};

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

ts
function isStringAssert(value: any): asserts value {
    if (typeof value !== 'string') {
        throw new Error(`value is not type string`);
    }
}
const toUpperCase = (text: any) => {
    text.touppercase(); // not error

    isStringAssert(text);

    text.touppercase(); // not error

    return text.toUpperCase();
};

Улучшена поддержка для типа never возвращаемого из функций

Основное предназначение типа never явным образом указывать что из вызываемой функции возврата никогда не произойдет. Как известно подобный сценарий может призойти по причине бесконечного цекла или выброса исключения.

До TypeScript v3.7, в случаях когда одна функция имеющая декларацию возвращаемого типа отличного от void прерывала нормальное выполнение программы за счет вызова функции с возвращающим типом never, выводу типов требовалось либо явного указания возврата с помощью оператора return, либо применения инструкции throw.

ts
// [TypeScript < v3.7]

interface User {
    name: string;
}

function critical(message: string): never {
    throw new Error(message);
}

/**
 * Несмотря что в случае вызова функции critical
 * возврата из функции validate не произойдет,
 * из-за непонимания этого вывод типов считает
 * что для функции validate забыли указать
 * возвращаемое значение.
 *
 * (!) [ошибка в аннотации возвращаемого типа]
 * Function lacks ending return statement and
 * return type does not include 'undefined'.
 */
function validate(data: any): User /**Error (!) */ {
    if (data && data.user) {
        return data.user;
    }

    critical(`Field "user" not found in object "data."`);
}

/**
 * Для устронения ошибки требуется явно
 * указать возвращаемое значение...
 */
function validate_a(data: any): User /**Error (!) */ {
    if (data && data.user) {
        return data.user;
    }

    return critical(`Field "user" not found in object "data."`);
}
/**
 * ...либо выбросить исключение.
 */
function validate_b(data: any): User /**Error (!) */ {
    if (data && data.user) {
        return data.user;
    }

    throw critical(`Field "user" not found in object "data."`);
}

Начиная с версии v3.7 вывод типов научился распознавать прерывание нормального хода программы без явного указания return или throw.

ts
// [TypeScript >= v3.7]

interface User {
    name: string;
}

function critical(message: string): never {
    throw new Error(message);
}

/**
 * Явного указания return или throw
 * больше не требуется.
 */
function validate(data: any): User {
    if (data && data.user) {
        return data.user;
    }

    critical(`Field "user" not found in object "data."`);
}

Проверка невызванных функций

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

ts
interface IUser {
    isAuthorized(): boolean;
}

function someAction(user: IUser) {
    /**
     * Разработчик подумал что isAuthorized
     * это поле или свойство объекта, но не метод.
     *
     * Учитывая многообразие языков программирования
     * с различными конвенциями именования, такая ошибка
     * не является надуманной для недавно пришедших в
     * ECMScript коммунити.
     */
    if (user.isAuthorized) {
        /**
         * гость смог выполнить действия требующие
         * привелегии авторизованного пользователя.
         */
    }
}

Поэтому начиная с TypeScript v3.7 компилятор расценивает подобные ситуации как ошибку.

ts
interface IUser {
    isAuthorized(): boolean;
}

function someAction(user: IUser) {
    /**
     * [TypeScript < v3.7]
     * > Ok! Трудно выявляемая ошибка,
     *
     * [TypeScript >= v3.7]
     * > Error!
     * This condition will always return true since the
     * function is always defined. Did you mean to call
     * it instead?
     */
    if (user.isAuthorized) {
    }
}

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

ts
interface IUser {
    name: string;

    isAuthorized(): boolean;
}

function someAction(user: IUser) {
    /**
     * [TypeScript >= v3.7]
     * > Error
     */
    if (user.isAuthorized) {
    }

    /**
     * name эквивалентно undefined
     * хотя в реальности должно
     * иметь значение 'guest'.
     */
    let name = user.isAuthorized ? user.name : 'guest';
}

Кроме того, оно не работает с необязательными членами и при установленным в false опции компилятора --strictNullChecks.

ts
interface IUser {
    isAuthorized?(): boolean; // необязательный член
}

function someAction(user: IUser) {
    /**
     * [TypeScript < v3.7]
     * > Ok! Трудно выявляемая ошибка,
     *
     * [TypeScript >= v3.7]
     * > Ok! Трудно выявляемая ошибка,
     */
    if (user.isAuthorized) {
    }
}

Также же ошибки не возникает если невызванная функция вызывается далее в условном блоке.

ts
interface IUser {
    isAuthorized(): boolean;
}

function someAction(user: IUser) {
    // Error
    if (user.isAuthorized) {
    }

    // Ok
    if (user.isAuthorized) {
        user.isAuthorized();
    }

    // Ok!, ???
    if (user.isAuthorized) {
        user.isAuthorized;
    }

    // Error
    if (user.isAuthorized) {
    } else {
        user.isAuthorized();
    }

    // Error
    if (user.isAuthorized) {
    }

    user.isAuthorized();
}

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

ts
interface IUser {
    isAuthorized(): boolean;
}

function someAction(user: IUser) {
    // Ok
    if (user.isAuthorized !== null) {
    }

    // Ok
    if (user.isAuthorized !== undefined) {
    }

    // Ok
    if (!!user.isAuthorized) {
    }
}

Рекурсивность для псевдононимов типов

Псевдонимы типов (type aliases) всегда имели строгие правила относительно рекурсии поскольку больше остальных могли привести к бесконечному обращению.

ts
type T = T; // Бесконечная рекурсия

Тем не менее относительно рекурсивности существовали правила, которые можно было обойти введя дополнительные интерфейсные типы (interface).

ts
// TypeScript < v3.7

type Json = string | number | boolean | null | JsonObject | JsonArray;

interface JsonObject {
    [property: string]: Json;
}

interface JsonArray extends Array<Json> {}

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

ts
// TypeScript >= v3.7

type Json =
    | string
    | number
    | boolean
    | null
    | { [property: string]: Json }
    | Json[];

Совместное использование флагов --declaration и --allowJs

До текущей версии TypeScript акивный флаг компилятора --declaration позволял генерировать файлы декларации .d.ts только из файлов имеющих расширение .ts и .tsx. Файлы декларации существуенно снижают нагрузку возложенную на компилятор, что является важным критерием для такого механизма как ссылки на проект, который существенно ускоряет процесс сборки больших приложений.

Но к сожалению флаг --declaration не совместим с другим таким важным флагом как --allowJs, который позволяет использовать в впроекте модули с расширением .js код в которых не поддается декларированию даже если объявления аннотированны с помощью JSDoc.

Начиная с TypeScript v3.7 это проблема устранена и теперь компилятор из-за всех сил будет пытаться описать структуру JavaScript кода с помощью типов, к тому же прибегая к помощи оставленной разработчиками при помощи JSDoc.

js
// [File: module.js]

export const VALUE = 5;
export const SUM = 5 + 5;
export const toString = (value) => value.toString();
ts
// [File: module.d.ts]

export const VALUE: 5;
export const SUM: number;
export function toString(value: any): any;
js
// [File: module.js]

export const VALUE = 5;
export const SUM = 5 + 5;
/**
 *
 * @param {string} value
 * @returns {string}
 */
export const toString = (value) => value.toString();
ts
// [File: module.d.ts]

export const VALUE: 5;
export const SUM: number;
export function toString(value: any): string;

@ts-nocheck в TypeScript файлах

Комментируемая директива @ts-nocheck, которая указанная в начале файла с расширением .js при активной опции --allowJs указывала компилятру что данный файл необходимо исключить из семантической проверке, ранее поддерживалась исключительно в JavaScript файлах. Начиная с TypeScript v3.7 данная директива также может указываться в файлах с расширением .ts и .tsx.