release

4.0

Вариативный кортеж

Представьте случай при котором перед вами стоит задача реализовать известную всем функцию объединения массивов и кортежей concat и менее известную tail, которая возвращает копию полученного в качестве аргумента массива, но только без первого элемента. На JavaScript описанные функции выглядели бы подобным образом -

js
function concat(a, b){
    return [...a, ...b];
}


function tail(a){
    let [, ...rest] = a;

    return rest;
}

Если бы при попытке добавть типизацию была потребность в аннотации пригодной исключительно для массивов, то дело бы обошлось привычным типом объединения (Union) -

ts
/**
 * Элементы возвращаемого массива могут
 * принадлежать к типу T или U
 */
function concat<T, U>(a: T[], b: U[]): Array<T | U>{
    return [...a, ...b];
}

/**
 * Возвращаемый массив может содержать
 * элементы принадлежащие к типу T, либо
 * в случаи когда входной массив содержит
 * только один элемент, к типу undefined.
 */
function tail<T>(a: T[]): Array<T> | Array<undefined>{
    let [, ...rest] = a;

    return rest;
}

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

ts
function concat<A0>(a: [A0], b: []): [A0];
function concat<A0, A1>(a: [A0, A1], b: []): [A0, A1];
function concat<A0, A1, A2>(a: [A0, A1, A2], b: []): [A0, A1, A2];

/**
 * И так до бесконечности!
 * И это только для первого параметра!
 */

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

Первое нововведение заключается в том, что механиз известный как spread (распростронение [...T]) в кортежах теперь может быть универсальным (generic). Это позволяет производить над типами массивов и кортежей операции более высокого порядка, что позволяет отказатся от перегрузок в пользу более продвинутого способа.

ts
/**
 * 0 - указываем что параметр типа должен обязательно быть потомком массива.
 * 1 - если T является потомком массива у которого существует первый элемент...
 * 2 - ... то выбираем остаточные элементы и определяем их как тип R.
 * 3 - при верности условия [1] определяем тип как тип R
 * 4 - при ложном условии [1] определяем тип как тип T (массива переданного в качестве аргумента)
 */
//        [      0      ]    [       1       [     2   ]] [3] [4]
type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : T;

function tail<T extends unknown[]>(arr:  readonly [...T] ): Tail<T> {
    const [, ...rest] = arr;

    return rest as Tail<T>;
}

let tuple = [0, 1, 2, 3] as const;
let array = ['hello', 'world'];

// let v0: string[]
let v0 = tail(array);

// let v1: [1, 2, 3]
let v1 = tail(tuple);

// let v2: [1, 2, 3, ...string[]]
let v2 = tail([...tuple, ...array] as const);

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

ts
type Strings = [string, string];
type Numbers = [number, number];

// type Mixed = [string, string, number, number]
type Mixed = [...Strings, ...Numbers];

Когда spread применяется к типу без известной длины (обычный массив ...number[]), то результатирующий тип также становится неограниченным и все типы слудующие после такого распростронения (обычный массив) образуют с ним тип объединение (Union).

ts
type Strings = [string, string];
type BooleanArray = boolean[];

// type Unbounded0 = [string, string, ...(boolean | symbol)[]]
type Unbounded0 = [...Strings, ...BooleanArray, symbol];

// type Unbounded1 = [string, string, ...(string | boolean | symbol)[]]
type Unbounded1 = [ ...Strings, ...BooleanArray, symbol, ...Strings]

Благодаря этим двум нововведениям теперь стало возможно типизировать функцию concat способом исключающим механизм перегрузок.

ts
type A = readonly unknown[];

function concat<T extends A, U extends A>(a: T, b: U): [...T, ...U] {
    return [...a, ...b];
}

// let v0: number[]
let v0 = concat([0, 1], [2, 3]);

// let v1: [0, 1, 2, 3]
let v1 = concat([0, 1] as const, [2, 3] as const);

// let v2: [0, 1, ...number[]]
let v2 = concat([0, 1] as const, [2, 3]);

// let v3: number[]
let v3 = concat([0, 1], [2, 3] as const);

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

ts
function carry(f, ...initialParams){
    return function (...restParams){
        return f(...initialParams, ...restParams);
    }
}

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

ts
type A = readonly unknown[];
type Carry<T extends A, U extends A, R> = (...restParams: [...T, ...U]) => R;

function carry<T extends A, U extends A, R>(f: Carry<T, U, R>, ...initialParams: T){
    return function (...restParams: U){
        return f(...initialParams, ...restParams);
    }
}


// использование

const f = (a: number, b: string, c: boolean) => {};

const f0 = carry(f, 5, ''); // Ok
const f1 = carry(f, 5, '', true); // Ok
const f2 = carry(f, 5, '', ''); // Error -> Argument of type '""' is not assignable to parameter of type 'boolean'.
const f3 = carry(f, 5, '', true, 5); // Error -> Expected 4 arguments, but got 5.

f0(true); // Ok
f0(1); // Error -> Argument of type '1' is not assignable to parameter of type 'boolean'.

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

Помеченные элементы кортежа

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

До текущей версии встретив кортеж наподобие [string, number] было совершенно не понятно что в дейтвительности представляют эти типы.

ts
// до версии 4.0

const f = (p: [string, number]) => {}

/**
 * автодополнение -> f(p: [string, number]): void
 * 
 * Совершенно не понятно чем конкретно являются
 * элементы представляемые типами string и number
 */
f0()

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

ts
// начиная с версии 4.0

const f = (p: [a: string, b: number]) => {};

/**
 * автодополнение -> f(p: [a: string, b: number]): void
 * 
 * Теперь мы знаем что функция ожидает не просто 
 * строку и число, а аргумент "a" и аргумент "b",
 * которые в реальном проекте будут иметь более
 * осмысленное смысловое значение, например "name" и "age".
 */
f1()

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

ts
const f = (p: [a: string, b: number]) => {
    let [c, d] = p;
};

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

ts
type T = [a: number, b: string, boolean]; // Error -> Tuple members must all have names or all not have names.ts(5084)

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

ts
declare function useState<T>(initialState: T):[state: T, setState: (state: T) => void];

/**
 * автокомплит -> useState(initialState: number): [state: number, setState: (state: number) => void]
 * 
 * Даже в отсутствии комментариев известно
 * что именно предполагается в возвращаемом значении!
 */
let [state, setState] = useState(0);

Операторы присваивания короткого замыкания

В большинстве языков, в том числе и JavaScript существует такое понятие как составные операторы присваивания (compound assignment operators) позволяющие совмещать операцию присваивания с помощью оператора = и какой-либо другой допустимой операции (+-*/! и т.д.) и тем самым значительно сокращать выражение.

ts
let a = 1;
let b = 2;

a += b; // тоже самое что a = a + b
a *= b; // тоже самое что a = a * b
// и т.д.

Множество существующих операторов имеют возможность быть совмещенными с оператором присваивания за исключением трех, часто применяемых таких оператора как логическое И (&&), логическое ИЛИ (||) и оператор нулевого слияния (??).

ts
a = a && b;
a = a || b;
a = a ?? b;

Поэтому с версии 4.0 TypeScript реализует такую возможность.

ts
let a = {};
let b = {};

a &&= b; // a && (a = b)
a ||= b; // a || (a = b);
a ??= b; // a !== null && a !== void 0 ? a : (a = b);

Вывод типов для полей класса по параметрам конструктора

До текущей версии при активном флаге noImplicitAny возникала ошибка если тело класса содержало поле без аннотации типа. И не спасало даже то, что они были инициализированны в конструкторе.

ts
class Square {
    /**
     * До версии 4.0 поля без аннотации вызывали ошибку .-
     * Member 'area' implicitly has an 'any' type.
     * Member 'sideLength' implicitly has an 'any' type.
     * 
     */
    area;
    sideLength;

    constructor(sideLength: number) {
        this.sideLength = sideLength;
        this.area = sideLength ** 2;
    }
}

Простыми словами вывод типов не был обучен выводить типы в подобных случаях. Но как можно догадатся больше для него это не проблема!

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

ts
class Square {
    /**
     * Начиная с версии 4.0 -
     * (property) Square.area: number
     * (property) Square.sideLength: number
     * 
     * Вывод типов видит что полю sideLength
     * присваивают значение с типом number, а
     * полю area результат выражения над числовыми
     * типами.
     */
    area;
    sideLength;

    constructor(sideLength: number) {
        this.sideLength = sideLength;
        this.area = sideLength ** 2;
    }
}

Не будет лишним сделать акцент на словах об инициализации в конструкторе, поскольку это условие является обязательным. При попытке инициализации полей вне тела конструктора будет вызвана ошибка, даже если инициализация производится в методе вызываемом из конструктора.

ts
class Square {
    /**
     * Error ->
     * Member 'area' implicitly has an 'any' type.
     * 
     */
    area;
    sideLength;

    constructor(sideLength: number) {
        this.sideLength = sideLength;
        this.init();
    }

    init(){
        this.area = this.sideLength ** 2;
    }
}

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

ts
class Square {
    /**
     * [1] ...вывод типов определяет принадлежность
     * поля sideLength как ->
     * 
     * (property) Square.sideLength: number | undefined
     */
    sideLength;

    constructor(sideLength: number) {
        /**
         * [0] Поскольку инициализация зависи от
         * условия выражения которое выполнится
         * только во время выполнения программы...
         */
        if (Math.random()) {
            this.sideLength = sideLength;
        }
    }

    get area() {
        /**
         * [2] Тем не менее возникает ошибка
         * поскольку операция возведения в степень
         * производится над значение которое может
         * быть undefined
         * 
         * Error ->
         * Object is possibly 'undefined'.
         */
        return this.sideLength ** 2;
    }
}

unknown как тип исключения блока catch

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

ts
/**
 * До версии 4.0
 */

try {
    throw new Error(`Error!`);
}catch(e){ // Ok
    e.meSSage.touppercase(); // Ошибка времени исполнения
}

try {

}catch(e: Error){ // Error -> [*]
    e.meSSage.touppercase();
}

try {
    
}catch(e: any){ // Error -> [*]
    e.meSSage.touppercase();
}

try {
    
}catch(e: unknown){ // Error -> [*]
    e.meSSage.touppercase();
}

/**
 * [*] Catch clause variable cannot have a type annotation.
 */

Начиная с текущей версии в аннотации исключения блока catch допускается указывать такие типы как unknown и any.

ts
/**
 * Начиная с версии 4.0
 */

try {
    throw new Error(`Error!`);
}catch(e){ // Ok
    e.meSSage.touppercase(); // Ошибка времени исполнения
}

try {

}catch(e: Error){ // Error -> Catch clause variable type annotation must be 'any' or 'unknown' if specified.
    e.meSSage.touppercase();
}

try {
    
}catch(e: any){ // Ok
    e.meSSage.touppercase(); // Ошибка времени исполнения
}

try {
    
} catch (e: unknown) { // Ok
    e.meSSage.touppercase(); // Ошибка времени исполнения
    e.message; // Error -> поскольку у типа unknown отсутствует свойство message
}

Тип any остается значением по умолчанию и пока остается для обратной совместимости. В будущем планируется добавить новый strict mode флаг, с помощь которого можно будет изменить тип исключения по умолчанию на более строгий unknown, который позволит избежать ошибок при помощи дополнительного уровня проверок.

ts
try {
    
} catch (e: unknown) { // Ok
    e.meSSage.touppercase(); // Ошибка времени исполнения
    e.message; // Error -> поскольку у типа unknown отсутствует свойство message

    if (e instanceof Error) {
        e.message; // Ok
    }
}

--noEmit совмещенный с --incremental

Начиная с версии 4.0 стало возможным использовать флаг --noEmit при инкрементальной сборке активируемой с помощью флага --incremental.

Пользовательская фабрика jsx фрагментов

До текущей версии было не возможно использовать react фрагменты при определении пользовательской фабрики в tsconfig.json "jsxFactory": "h" или с помощью инлайн дерективы /** @jsx dom */. По этой причине, начиная с текущей версии в TypeScript появилась возможность определять пользувательскую фабрику фрагментов с помощью новой опции jsxFragmentFactory значение Fragment которой выполняет компиляцию с помощью определенного вместо встроенного в react механизма React.Fragment.

json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "jsx": "react",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  }
}

Кроме того для реализации того же поведения можно воспользоватся новой инлайн директивой /** @jsxFrag */.

ts
/**
 * Данный код будет скомпилирован
 * с помощью механизмов библиотеки preact
 * подменяющих React.createElement и
 * React.Fragment
 */

/** @jsx h */
/** @jsxFrag Fragment */

import { h, Fragment } from "preact";

let Title = <>
    <h1>Title</h1>
    <h2>Subtitle</h2>
</>;

Добавление комментарной директивы @deprecated

Начиная с текущей версии в TypeScript появилась возможность помечать код как устаревший с помощью комментарной директивы /** @deprecated */, что позволит современным ide подсвечивать устаревшее api.

ts
class T {
    /** @deprecated use method new()*/
    old(){}
    new(){}
}

let t = new T();
/**
 * @deprecated — use method new()
 * 'old' is deprecatedts(6385)
 */
t.old();

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

Изменениям в основной библиотеке lib.d.ts подверглись типы описывающие dom api. Наиболее значимое изменение удаление document.origin которое поддерживалось исключительно для старых версий IE и Safari. Взамен MDN рекомендует использовать self.origin.

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Переопределение аксессоров полем и наоборот теперь является ошибкой

Начиная с версии 3.7 был введен флаг useDefineForClassFields активация которого запрещала переопределение аксессоров полями и полей аксессорами при реализации механизма наследования (extends). Начиная с текущей версии поведение равное активируемому флагом useDefineForClassFields становится неотключаемым.

ts
class Base {
    get value() {
        return 'value';
    }
    set value(value: string) {
        
    }
}

class Derived extends Base {
    /**
     * Error ->
     * 
     * 'value' is defined as an accessor in class 'Base',
     * but is overridden here in 'Derived'
     * as an instance property.
     */
    value = 'value';
}
ts
class Base {
     value = 'value';
}

class Derived extends Base {
    /**
     * Error ->
     * 
     * 'value' is defined as a property in class 'Base',
     * but is overridden here in 'Derived' as an accessor.
     */
   
    get value() {
        return 'value';
    }
    set value(value: string) {
        
    }
}

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Операнды для delete должны быть необязательными

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

ts
type T0 = {
    field: any;
}

const f0 = (o: T0) => delete o.field; // Ok


type T1 = {
    field: unknown;
}

const f1 = (o: T1) => delete o.field; // Ok


type T2 = {
    field: never;
}

const f2 = (o: T2) => delete o.field; // Ok


type T3 = {
    field?: number;
}

const f3 = (o: T3) => delete o.field; // Ok


type T4 = {
    field: number;
}

const f4 = (o: T4) => delete o.field; // Error -> The operand of a 'delete' operator must be optional.

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Изменение фабричного api Nodejs

Начиная с текущей версии TypeScript отказывается от старых фабричных функций создания nodejs ast узлов в пользу нового api.