4.1
Изменение механизма проверки индексной сигнатуры
Поскольку механизм позволяющий определение индексной сигнатуры не способен отслеживать идентификаторы (имена) полей определенных динамически, такой подход не считается типобезопасным.
type T = { [key: string]: number | string; } function f(p: T) { /** * Обращение к несуществующим полям */ p.bad.toString(); // Ok -> Ошибка времени исполнения p[Math.random()].toString(); // Ok -> Ошибка времени исполнения }
Для решения данной проблемы был создан механизм активируемый с помощью нового флага --noUncheckedIndexedAccess
ожидающий в качестве значения true
либо false
. Активация механизма позволяет обращаться к динамическим полям только после подтверждения их наличия в объекте, а также совместно при совместном использовании с такими операторами, как оператор опциональной последовательности ?.
и опциональный оператор !.
.
// @filename: tsconfig.json { "compilerOptions": { "noUncheckedIndexedAccess": true } }
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 -> Ошибка не возникнет }
Кроме этого, влияние данного механизма распространяется также и на массивы. В случае с массивом не получится избежать аналогичной ошибки при попытке обращения к его элементам при помощи индексной сигнатуры.
function f(array: string[]) { for(let i = 0; i < array.length; i++){ array[i].toString(); // Error -> Object is possibly 'undefined'. } }
Поскольку данный флаг может потребовать внесения значительных изменений в существующих проектах, он не был включён в группировку --strict
и активируется индивидуально.
Шаблонный литеральный строковый тип
Думаю, что каждый знакомый с TypeScript не понаслышке, не в состоянии забыть пользу от строковых литеральных типов которые эффективно помогают выявлять орфографические ошибки строковых значений на этапе компиляции...
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"'.
...а также используются при определении новых типов выступают в качестве ключей сопоставленных типов.
type Animation = { [K in "ease" | "ease-in" | "ease-out"]?: boolean; }
И вот, начиная с версии v4.1
они нашли новое применение в удивительном механизме получившем название Шаблонный литеральный строковый тип.
Шаблонный литеральный строковый тип — это тип, позволяющий на основе литеральных строковых типах динамически определять новый литеральный строковый тип. Простыми словами, это известный по JavaScript механизм создания шаблонных строк только для типов.
type Type = "Type"; type Script = "Script"; /** * type Message = "I ❤️ TypeScript" */ type Message = `I ❤️ ${Type}${Script}`;
Но вся мощь данного типа раскрывается в момент определение нового типа на основе объединения (union
). В подобных случаях новый тип будет также представлять объединение элементы которого представляют все возможные варианты полученные на основе исходного объединения.
type Sides = "top" | "right" | "bottom" | "left"; /** * type PaddingSides = "padding-top" | "padding-right" | "padding-bottom" | "padding-left" */ type PaddingSides = `padding-${Sides}`;
Аналогичное поведение будет справедливо и для нескольких типов объединения.
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
. Данные операторы применяются непосредственно к литеральному строковому типу который указывается справа от него.
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
указываемого после строкового перечисления.
type T = { [K in STRING_VALUES as NEW_KEY]: K // K преобразованный }
Таким образом совмещая данный механизм с шаблонными литеральными строковыми типами можно добиться переопределения исходных ключей.
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>
Рекурсивные условные типы
При разработке программ часто возникают потребности в создании значений при помощи рекурсии в основе которой лежит логическое условие.
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 делает послабления на установленные правила относительно рекурсивных типов.
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 конфигурация проекта могла бы выглядеть следующим образом.
// tsconfig.json { "compilerOptions": { "module": "esnext", "target": "es2015", "jsx": "react-jsx", "strict": true }, "include": [ "./**/*" ] }
// tsconfig.dev.json { "extends": "./tsconfig.json", "compilerOptions": { "jsx": "react-jsxdev" } }
Поддержка тега @see для JSDoc
Теперь JSDoc поддерживает тег @see
упрощающий работу с кодом за счет возможности перехода к определению (go-to-definition).
// @filename: animals.ts export class Animal { } export class Fish { } export class Bird { } // @filename: index.js import * as animals from './animals'; /** * @see animals.Bird */ function related() { }
[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] abstract больше не совместим с async
Начиная с текущей версии абстрактные методы не могут помечаться ключевым словом async
.
[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] any и unknown доминируют в ложных позициях
Если в условии с логическим И (&&
) значение левого операнда принадлежало к типу any
или unknown
, то вывод типа выводил тип правого операнда. Начиная с текущей версии в подобных условиях всегда будут выводиться any
или unknown
.
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
файлах выглядели привычным образом.
/** * До компиляции */ // @filename: ./utils.ts export const toLowerCase = (text: string) => text.toLowerCase(); // @filename: ./index.js export * from "./utils";
/** * После компиляции .d.ts */ declare module "utils" { export const toLowerCase: (text: string) => string; } declare module "index" { export * from "utils"; }
Начиная с текущей версии при совместном использовании параметров --declaration
и --outFile
необходимо задавать значение (имя пакета) параметру bundledPackageName
. В противном случае возникнет ошибка - The
bundledPackageName option must be provided when using outFile and node module resolution with declaration emit.
.
{ "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/*" ] }
/** * После компиляции .d.ts */ declare module "my-lib/utils" { export const toLowerCase: (text: string) => string; } declare module "my-lib/index" { export * from "utils"; }
[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] параметры resolve теперь обязательные
До текущей версии функцию resolve
, участвующей в работе логики Promise
, можно было вызывать без аргументов, поскольку её параметры описаны, как необязательные.
new Promise(resolve => { resolve(); // Ok });
Начиная с текущей версии описание функции resolve
изменило поведение для её параметров сделав их обязательными. Теперь при отсутствии параметров будет возникать ошибка.
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
.
new Promise<void>(resolve => { resolve(); // Ok });