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.ts
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.ts
export * from "./utils";
/**
* После компиляции .d.ts
*/
declare module "utils" {
export const toLowerCase: (text: string) => string;
}
declare module "index" {
export * from "utils";
}
Начиная с текущей версии при совместном использовании параметров --declaration
и --outFile
необходимо задавать значение (имя пакета) параметру bundledPackageName
. В противном случае возникнет ошибка - The
bundledPackageNameoption 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
});