beta

4.1

Изменение механизма проверки индексной сигнатуры

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

ts
type T = {
    [key: string]: number | string;
}

function f(p: T) {
    /**
     * Обращение к несуществующим полям
     */
    p.bad.toString(); // Ok -> Ошибка времени исполнения
    p[Math.random()].toString(); // Ok -> Ошибка времени исполнения
}

Для решения данной проблемы был создан механизм активируемый с помощью нового флага --noUncheckedIndexedAccess ожидающий в качестве значения true либо false. Активация механизма позволяет обращаться к динамическим полям только после подтверждения их наличия в объекте, а также совместно при совместном использовании с такими операторами, как оператор опциональной последовательности ?. и опциональный оператор !..

json
// @filename: tsconfig.json

{
    "compilerOptions": {
        "noUncheckedIndexedAccess": true
    }
}
ts
type T = {
  [key: string]: number | string;
}


function f(p: T) {
  /**
   * Обращение к несуществующим полям
   */
  p.bad.toString(); // Error -> Object is possibly 'undefined'.ts(2532)
  p[Math.random()].toString(); // Error -> Object is possibly 'undefined'.ts(2532)


  // Проверка наличия поля bad
  if("bad" in p){
      p.bad?.toString(); // Ok
  }

  // Использование опционального оператора
  p[Math.random()]!.toString(); // Ok -> ошибка во время выполнения

  p[Math.random()]?.toString();  // Ok -> Ошибка не возникнет
}

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

ts
function f(array: string[]) {
    for(let i = 0; i < array.length; i++){
        array[i].toString(); // Error -> Object is possibly 'undefined'.
    }
}

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

Шаблонный литеральный строковый тип

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

ts
function setAnimation(animationType: "ease" | "ease-in" | "ease-out"){
    // ... какая-то логика
}
setAnimation("ease"); // Error -> Argument of type '"ease"' is not assignable to parameter of type '"ease" | "ease-in" | "ease-out"'.

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

ts
type Animation = {
    [K in "ease" | "ease-in" | "ease-out"]?: boolean;
}

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

Шаблонный литеральный строковый тип — это тип, позволяющий на основе литеральных строковых типах динамически определять новый литеральный строковый тип. Простыми словами, это известный по JavaScript механизм создания шаблонных строк только для типов.

ts
type Type = "Type";
type Script = "Script";

/**
 * type Message = "I ❤️ TypeScript"
 */
type Message = `I ❤️ ${Type}${Script}`;

Но вся мощь данного типа раскрывается в момент определение нового типа на основе объединения (union). В подобных случаях новый тип будет также представлять объединение элементы которого представляют все возможные варианты полученные на основе исходного объединения.

ts
type Sides = "top" | "right" | "bottom" | "left";

/**
 * type PaddingSides = "padding-top" | "padding-right" | "padding-bottom" | "padding-left"
 */
type PaddingSides = `padding-${Sides}`;

Аналогичное поведение будет справедливо и для нескольких типов объединения.

ts
type AxisX = "top" | "bottom";
type AxisY = "left" | "right";


/**
 * type Sides = "top-left" | "top-right" | "bottom-left" | "bottom-right"
 */
type Sides = `${AxisX}-${AxisY}`;

/**
 * type BorderRadius = "border-top-left-radius" | "border-top-right-radius" | "border-bottom-left-radius" | "border-bottom-right-radius"
 */
type BorderRadius = `border-${Sides}-radius`;

Поскольку с высокой долей вероятности в подобных операциях потребуется трансформация регистра строк, создателями данного механизма так же были добавлены новые операторы преобразования uppercase, lowercase, capitalize и uncapitalize. Данные операторы применяются непосредственно к литеральному строковому типу который указывается справа от него.

ts
type A = `${uppercase "AbCd"}`; // type A = "ABCD"
type B = `${lowercase "AbCd"}`; // type B = "abcd"
type C = `${capitalize "abcd"}`; // type C = "Abcd"
type D = `${uncapitalize "Abcd"}`; // type D = "abcd"

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

Переопределение ключей в сопоставленных типах

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

ts
type T = {
    [K in STRING_VALUES as NEW_KEY]: K // K преобразованный
}

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

ts
type ToGetter<T> = `get${capitalize T}`;
type Getters<T> = {
    [K in keyof T as ToGetter<K>]: () => T[K];
}

type Person = {
    name: string;
    age: number;
}

/**
 * type T = {
 *  getName: () => string;
 *  getAge: () => number;
 * }
 */
type T = Getters<Person>

Рекурсивные условные типы

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

ts
function flat(value){
    if(Array.isArray(value)){ // логическое условие
        return value.reduce((result, current) => [
            ...result, 
            ...flat(current)
        ], []);
    }

    return [value];
}

flat([0, [1, [2]], 3]); // [0, 1, 2, 3]

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

ts
type GetItemType<T> = T extends ReadonlyArray<infer U> ? GetItemType<U> : T;

declare function flat<T extends readonly unknown[]>(value: T): GetItemType<T>[];


let result = flat([0, [1, [2]], 3]); // let result: number[] = [0, 1, 2, 3]

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

paths без baseUrl

Ранее указание псевдонима для пути с помощью paths требовало также установление значения параметру baseUrl. Это не позволяло автоимпорту указывать правильные пути. Поэтому начиная с текущей версии paths больше не зависит от параметра baseUrl.

checkJs не требует активации allowJs

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

jsx фабрики для React 17

Текущая версия TypeScript получила поддержку будущих jsx и jsxs фабрик предполагаемых React 17. Для этого были реализованны две новые опции react-jsx и react-jsxdev.

При разделении конфигурации на production и development конфигурация проекта могла бы выглядеть следующим образом.

json
// tsconfig.json
{
    "compilerOptions": {
        "module": "esnext",
        "target": "es2015",
        "jsx": "react-jsx",
        "strict": true
    },
    "include": [
        "./**/*"
    ]
}
json
// tsconfig.dev.json
{
    "extends": "./tsconfig.json",
    "compilerOptions": {
        "jsx": "react-jsxdev"
    }
}

Поддержка тега @see для JSDoc

Теперь JSDoc поддерживает тег @see упрощающий работу с кодом за счет возможности перехода к определению (go-to-definition).

ts
// @filename: animals.ts
export class Animal { }
export class Fish { }
export class Bird { }

// @filename: index.ts
import * as animals from './animals';

/**
 * @see animals.Bird
 */
function related() { }

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] abstract больше не совместим с async

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

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] any и unknown доминируют в ложных позициях

Если в условии с логическим И (&&) значение левого операнда принадлежало к типу any или unknown, то вывод типа выводил тип правого операнда. Начиная с текущей версии в подобных условиях всегда будут выводиться any или unknown.

ts
declare let a: any;
declare let n: number;
declare let u: unknown;

/**
 * Вывод типов видит так ->
 *         <v4.1  |  >=v4.1
 * let v0: number    any
 * let v1: number    unknown
 * let v2: any       any
 * let v3: unknown   unknown
 */
let v0 = a && n;
let v1 = u && n;
let v2 = n && a;
let v3 = n && u;

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] --declaration и --outFile требуют имя корневого пакета

До текущего момента при совместном использовании --declaration и --outFile пути в .d.ts файлах выглядели привычным образом.

ts
/**
 * До компиляции
 */

// @filename: ./utils.ts
export const toLowerCase = (text: string) => text.toLowerCase();

// @filename: ./index.ts
export * from "./utils";
ts
/**
 * После компиляции .d.ts
 */

declare module "utils" {
    export const toLowerCase: (text: string) => string;
}
declare module "index" {
    export * from "utils";
}

Начиная с текущей версии при совместном использовании параметров --declaration и --outFile необходимо задавать значение (имя пакета) параметру bundledPackageName. В противном случае возникнет ошибка - ThebundledPackageNameoption must be provided when using outFile and node module resolution with declaration emit..

json
{
    "compilerOptions": {
        "module": "amd",
        "target": "esnext",
        "jsx": "preserve",
        "sourceMap": true,

        "declaration": true,
        "outFile": "./dest/my-lib.js",
        "bundledPackageName": "my-lib"
    },
    "include": ["./src/"],
    "exclude": [
        "node_modules",
        "**/node_modules/*"
    ]
}
ts
/**
 * После компиляции .d.ts
 */

declare module "my-lib/utils" {
    export const toLowerCase: (text: string) => string;
}
declare module "my-lib/index" {
    export * from "utils";
}

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

До текущей версии функцию resolve, участвующей в работе логики Promise, можно было вызывать без аргументов, поскольку её параметры описаны, как необязательные.

ts
new Promise(resolve => {
    resolve(); // Ok
});

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

ts
new Promise(resolve => {
    resolve(); // Error -> Expected 1 arguments, but got 0. Did you forget to include 'void' in your type argument to 'Promise'?
});

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

ts
new Promise<void>(resolve => {
    resolve(); // Ok 
});