Hello, world!
Представляю вашему вниманию перевод второй части этой замечательной статьи, посвященной возможностям JS и TS последних трех лет, которые вы могли пропустить.
В первой части мы говорили о возможностях JS, во второй поговорим о возможностях TS.
Это вторая часть.
Обратите внимание: названия многих возможностей — это также ссылки на соответствующие разделы документации TypeScript.
TypeScript
Основы (контекст для дальнейшего изложения)
Дженерики / Generics: позволяют определять (передавать) параметры типов (type parameters). Это позволяет типам быть одновременно общими и типобезопасными (typesafe). Дженерики следует использовать вместо any
или unknown
везде, где это возможно.
// Без дженериков:
function getFirstUnsafe(list: any[]): any {
return list[0];
}
const firstUnsafe = getFirstUnsafe(['test']); // any
// С дженериками:
function getFirst<Type>(list: Type[]): Type {
return list[0];
}
const first = getFirst<string>(['test']); // string
// В данном случае параметр типа может быть опущен, поскольку тип автоматически выводится (inferred) из аргумента
const firstInferred = getFirst(['test']); // string
// Параметр типа может ограничиваться с помощью ключевого слова `extends`
class List<T extends string | number> {
private list: T[] = [];
get(key: number): T {
return this.list[key];
}
push(value: T): void {
this.list.push(value);
}
}
const list = new List<string>();
list.push(9);
// TypeError: Argument of type 'number' is not assignable to parameter of type 'string'.
const booleanList = new List<boolean>();
// TypeError: Type 'boolean' does not satisfy the constraint 'string | number'.
До TS4 (возможности, о которых многие не знают)
Утилиты типов / Utility types: позволяют легко создавать типы на основе других типов.
interface Test {
name: string;
age: number;
}
// `Partial` делает все свойства опциональными
type TestPartial = Partial<Test>;
// { name?: string | undefined; age?: number | undefined; }
// `Required` делает все свойства обязательными
type TestRequired = Required<TestPartial>;
// { name: string; age: number; }
// `Readonly` делает все свойства доступными только для чтения
type TestReadonly = Readonly<Test>;
// { readonly name: string; readonly age: string }
// `Record` облегчает типизацию объектов. Является более предпочтительным способом, чем использование сигнатур доступа по индексу (index signatures)
const config: Record<string, boolean> = { option: false, anotherOption: true };
// `Pick` извлекает указанные свойства
type TestLess = Pick<Test, 'name'>;
// { name: string; }
type TestBoth = Pick<Test, 'name' | 'age'>;
// { name: string; age: string; }
// `Omit` игнорирует указанные свойства
type TestFewer = Omit<Test, 'name'>;
// { age: string; }
type TestNone = Omit<Test, 'name' | 'age'>;
// {}
// `Parameters` извлекает типы параметров функции
function doSmth(value: string, anotherValue: number): string {
return 'test';
}
type Params = Parameters<typeof doSmth>;
// [value: string, anotherValue: number]
// `ReturnType` извлекает тип значения, возвращаемого функцией
type Return = ReturnType<typeof doSmth>;
// string
// Существует много других утилит
Условные типы / Conditional types: позволяют определять типы условно на основе совпадения/расширения других типов. Читаются как тернарные операторы в JS.
// Извлекает тип из массива или возвращает переданный тип
type Flatten<T> = T extends any[] ? T[number] : T;
// Извлекает тип элемента
type Str = Flatten<string[]>; //string
// Возвращает сам тип
type Num = Flatten<number>; // number
Вывод типов с помощью условных типов: некоторые дженерики могут быть выведены на основе кода. Для реализации условий на основе выводимых типов используется ключевое слово extends
. Оно позволяет определять временные (temporary) типы:
// Перепишем последний пример
type FlattenOld<T> = T extends any[] ? T[number] : T;
// Вместо индексации массива, мы можем просто вывести из него тип `Item`
type Flatten<T> = T extends (infer Item)[] ? Item : T;
// Что если мы хотим написать тип, извлекающий тип, возвращаемый функцией, или `undefined`?
type GetReturnType<Type> = Type extends (...args: any[]) => infer Return ? Return : undefined;
type Num = GetReturnType<() => number>; // number
type Str = GetReturnType<(x: string) => string>; // string
type Bools = GetReturnType<(a: boolean, b: boolean) => void>; // undefined
Необязательные и прочие (rest) элементы кортежа: опциональные элементы кортежа обозначаются с помощью ?
, прочие — с помощью ...
:.
// Предположим, что длина кортежа может быть от 1 до 3
const list: [number, number?, boolean?] = [];
list[0] // number
list[1] // number | undefined
list[2] // boolean | undefined
list[3] // TypeError: Tuple type '[number, (number | undefined)?, (boolean | undefined)?]' of length '3' has no element at index '3'.
// Кортежи можно создавать на основе других типов
// Оператор `rest` можно использовать, например, для добавления элемента определенного типа в начало массива
function padStart<T extends any[]>(arr: T, pad: string): [string, ...T] {
return [pad, ...arr];
}
const padded = padStart([1, 2], 'test'); // [string, number, number]
Абстрактные классы / Abstract classes: абстрактные классы и абстрактные методы классов обозначаются с помощью ключевого слова abstract
. Такие классы (методы) не могут инстанцироваться напрямую.
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('Гуляет...');
}
}
// Абстрактные методы должны быть реализованы при расширении класса
class Cat extends Animal {}
// CompileError: Non-abstract class 'Cat' does not implement inherited abstract member 'makeSound' from class 'Animal'
class Dog extends Animal {
makeSound() {
console.log('Гав!');
}
}
// Абстрактные классы не могут инстанцироваться (как интерфейсы), а абстрактные методы не могут вызываться напрямую
new Animal();
// CompileError: Cannot create an instance of an abstract class
const dog = new Dog().makeSound(); // Гав!
Сигнатуры конструктора / Construct signatures: позволяют определять типы конструкторов классов за пределами классов. В большинстве случаев вместо сигнатур конструкторов используются абстрактные классы.
interface MyInterface {
name: string;
}
interface ConstructsMyInterface {
new(name: string): MyInterface;
}
class Test implements MyInterface {
name: string;
constructor(name: string) {
this.name = name;
}
}
class AnotherTest {
age: number;
}
function makeObj(n: ConstructsMyInterface) {
return new n('hello!');
}
const obj = makeObj(Test); // Test
const anotherObj = makeObj(AnotherTest);
// TypeError: Argument of type 'typeof AnotherTest' is not assignable to parameter of type 'ConstructsMyInterface'.
Утилита типа ConstructorParameters
: извлекает типы параметров конструктора класса (но не тип самого класса).
interface MyInterface {
name: string;
}
interface ConstructsMyInterface {
new(name: string): MyInterface;
}
class Test implements MyInterface {
name: string;
constructor(name: string) {
this.name = name;
}
}
function makeObj(test: ConstructsMyInterface, ...args: ConstructorParameters<ConstructsMyInterface>) {
return new test(...args);
}
makeObj(Test); // TypeError: Expected 2 arguments, but got 1.
const obj = makeObj(Test, 'test'); // Test
TS4.0
Типы вариативных кортежей / Variadic tuple types: прочие (rest) элементы кортежей могут быть общими (generic). Разрешается использование нескольких прочих элементов.
// Что если нам нужна функция, комбинирующая 2 кортежа неизвестной длины?
// Как определить возвращаемый тип?
// Раньше:
// Приходилось писать перегрузки (overloads)
declare function concat(arr1: [], arr2: []): [];
declare function concat<A>(arr1: [A], arr2: []): [A];
declare function concat<A, B>(arr1: [A], arr2: [B]): [A, B];
declare function concat<A, B, C>(arr1: [A], arr2: [B, C]): [A, B, C];
declare function concat<A, B, C, D>(arr1: [A], arr2: [B, C, D]): [A, B, C, D];
declare function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
declare function concat<A, B, C>(arr1: [A, B], arr2: [C]): [A, B, C];
declare function concat<A, B, C, D>(arr1: [A, B], arr2: [C, D]): [A, B, C, D];
declare function concat<A, B, C, D, E>(arr1: [A, B], arr2: [C, D, E]): [A, B, C, D, E];
declare function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
declare function concat<A, B, C, D>(arr1: [A, B, C], arr2: [D]): [A, B, C, D];
declare function concat<A, B, C, D, E>(arr1: [A, B, C], arr2: [D, E]): [A, B, C, D, E];
declare function concat<A, B, C, D, E, F>(arr1: [A, B, C], arr2: [D, E, F]): [A, B, C, D, E, F];
// Согласитесь, что выглядит это не очень хорошо
// Также можно было комбинировать типы
declare function concatBetter<T, U>(arr1: T[], arr2: U[]): (T | U)[];
// Но это приводило к типу (T | U)[]
// Сейчас:
// Тип вариативного кортежа позволяет легко комбинировать типы с сохранением информации о длине кортежа
declare function concatNew<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U];
const tuple = concatNew([23, 'hey', false] as [number, string, boolean], [5, 99, 20] as [number, number, number]);
console.log(tuple[0]); // 23
const element: number = tuple[1];
// TypeError: Type 'string' is not assignable to type 'number'.
console.log(tuple[6]);
// TypeError: Tuple type '[23, "hey", false, 5, 99, 20]' of length '6' has no element at index '6'.
Помеченные элементы кортежа / Labeled tuple elements: элементы кортежа могут быть именованными, например [start: number, end: number]
. Если один элемент является именованным, то остальные элементы также должны быть именованными.
type Foo = [first: number, second?: string, ...rest: any[]];
declare function someFunc(...args: Foo);
Вывод типа свойства класса из конструктора: при установке свойства в конструкторе тип свойства выводится автоматически.
class Animal {
// Раньше тип объявляемого свойства должен быть определяться вручную
name;
constructor(name: string) {
this.name = name;
console.log(this.name); // string
}
}
Поддержка тега deprecated JSDoc:
/** @deprecated message */
type Test = string;
const test: Test = 'dfadsf'; // TypeError: 'Test' is deprecated.
TS4.1
Типы шаблонных литералов / Template literal types: позволяют определять сложные строковые типы, например, путем комбинации нескольких строковых литералов.
type VerticalDirection = 'top' | 'bottom';
type HorizontalDirection = 'left' | 'right';
type Direction = `${VerticalDirection} ${HorizontalDirection}`;
const dir1: Direction = 'top left';
const dir2: Direction = 'left';
// TypeError: Type '"left"' is not assignable to type '"top left" | "top right" | "bottom left" | "bottom right"'.
const dir3: Direction = 'left top';
// TypeError: Type '"left top"' is not assignable to type '"top left" | "top right" | "bottom left" | "bottom right"'.
// Комбинироваться также могут дженерики и утилиты типов
declare function makeId<T extends string, U extends string>(first: T, second: U): `${Capitalize<T>}-${Lowercase<U>}`;
// Предположим, что мы хотим, чтобы ключи объекта начинались с нижнего подчеркивания
const obj = { value1: 0, value2: 1, value3: 3 };
const newObj: { [Property in keyof typeof obj as `_${Property}`]: number };
// { _value1: number; _value2: number; _value3: number; }
Рекурсивные условные типы: условные типы можно использовать внутри их определений. Это позволяет распаковывать типы бесконечно вложенных значений.
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
type P1 = Awaited<string>; // string
type P2 = Awaited<Promise<string>>; // string
type P3 = Awaited<Promise<Promise<string>>>; // string
Поддержка тега see JSDoc:
const originalValue = 1;
/**
* Копия другого значения
* @see originalValue
*/
const value = originalValue;
explainFiles
: при использовании флага CLI --explainFiles
или установке одноименной настройки в файле tsconfig.json
, TS сообщает, какие файлы и почему компилируются. Может быть полезным для отладки. Обратите внимание: для уменьшения вывода (output) в больших и сложных проектах можно, например, использовать команду tsc --explainFiles | less
.
Явное определение неиспользуемых переменных: при деструктуризации неиспользуемые переменные могут быть помечены с помощью нижнего подчеркивания. Это предотвращает соответствующую ошибку.
const [_first, second] = [3, 5];
console.log(second);
// или даже короче
const [_, value] = [3, 5];
console.log(value);
TS4.3
Разделение типов аксессоров: при определении аксессоров get/set тип записи/set может быть отделен от типа чтения/get. Это позволяет сеттерам принимать значения разных типов.
class Test {
private _value: number;
get value(): number {
return this._value;
}
set value(value: number | string) {
if (typeof value === 'number') {
this._value = value;
return;
}
this._value = parseInt(value, 10);
}
}
override
: индикатор перезаписи наследуемого класса. Используется для обеспечения типобезопасности в сложных паттернах наследования. Вместо ключевого слова override
можно использовать одноименный декоратор.
class Parent {
getName(): string {
return 'name';
}
}
class NewParent {
getFirstName(): string {
return 'name';
}
}
class Test extends Parent {
override getName(): string {
return 'test';
}
}
class NewTest extends NewParent {
override getName(): string { // TypeError: This member cannot have an 'override' modifier because it is not declared in the base class 'NewParent'.
return 'test';
}
}
Статические сигнатуры доступа по индексу / Static index signatures:
// Раньше:
class Test {}
Test.test = '';
// TypeError: Property 'test' does not exist on type 'typeof Test'.
// Сейчас:
class NewTest {
static [key: string]: string;
}
NewTest.test = '';
Поддержка тега link JSDoc:
const originalValue = 1;
/**
* Копия {@link originalValue}
*/
const value = originalValue;
TS4.4
exactOptionalPropertyTypes
: использование флага CLI --exactOptionalPropertyTypes
или установка одноименной настройки в файле tsconfig.json
запрещает неявную неопределенность поля — вместо property?: string
следует использовать property: string | undefined
.
class Test {
name?: string;
age: number | undefined;
}
const test = new Test();
test.name = undefined;
// TypeError: Type 'undefined' is not assignable to type 'string' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the type of the target.
test.age = undefined;
console.log(test.age); // undefined
TS4.5
Утилита типа Awaited
: извлекает тип значения бесконечно вложенных промисов. Это также улучшает вывод типов для Promise.all()
.
type P1 = Awaited<string>; // string
type P2 = Awaited<Promise<string>>; // string
type P3 = Awaited<Promise<Promise<string>>>; // string
Модификатор type
в именованном импорте: индикатор того, что значение требуется только для проверки типов и может быть удалено при компиляции.
// Раньше:
// Импорт значений и типов приходилось разделять во избежание импорта типов после компиляции
import { something } from './file';
import type { SomeType } from './file';
// Сейчас:
// Значения и типы могут импортироваться с помощью одной инструкции
import { something, type SomeType } from './file';
Утверждения const
/ const
assertions: позволяют корректно типизировать константы как литеральные типы. Это может использоваться во многих случаях и существенно повышает точность типизации. Это также делает объекты и массивы readonly
, что предотвращает их мутации.
// Раньше:
const obj = { name: 'foo', value: 9, toggle: false };
// { name: string; value: number; toggle: boolean; }
// Полю может присваиваться любое значение соответствующего типа
obj.name = 'bar';
const tuple = ['name', 4, true]; // (string | number | boolean)[]
// Длина кортежа и тип каждого элемента неизвестны
// Могут присваиваться любые значения соответствующих типов
tuple[0] = 0;
tuple[3] = 0;
// Сейчас:
const objNew = { name: 'foo', value: 9, toggle: false } as const;
// { readonly name: "foo"; readonly value: 9; readonly toggle: false; }
// Значения полей доступны только для чтения (не могут модифицироваться)
objNew.name = 'bar';
// TypeError: Cannot assign to 'name' because it is a read-only property.
const tupleNew = ['name', 4, true] as const; // readonly ["name", 4, true]
// Длина кортежа и тип каждого элемента теперь известны
tupleNew[0] = 0;
// TypeError: Cannot assign to '0' because it is a read-only property.
tupleNew[3] = 0;
// TypeError: Index signature in type 'readonly ["name", 4, true]' only permits reading.
Автозавершение методов классов:
TS4.6
Улучшение вывода типов при доступе по индексу: более точный вывод типов при доступе по ключу в рамках одного объекта.
interface AllowedTypes {
'number': number;
'string': string;
'boolean': boolean;
}
// `UnionRecord` определяет типы значений полей с помощью `AllowedTypes`
type UnionRecord<AllowedKeys extends keyof AllowedTypes> = { [Key in AllowedKeys]:
{
kind: Key;
value: AllowedTypes[Key];
logValue: (value: AllowedTypes[Key]) => void;
}
}[AllowedKeys];
// `logValue` принимает только значения типа `UnionRecord`
function processRecord<Key extends keyof AllowedTypes>(record: UnionRecord<Key>) {
record.logValue(record.value);
}
processRecord({
kind: 'string',
value: 'hello!',
// `value` может иметь тип `string | number | boolean`,
// но в данном случае правильно выводится тип `string`
logValue: value => {
console.log(value.toUpperCase());
}
});
Флаг CLI --generateTrace
: указывает TS генерировать файл, содержащий подробности проверки типов и процесса компиляции. Может быть полезным для оптимизации сложных типов.
TS4.7
Поддержка модулей ES в Node.js: для типобезопасного использования модулей ES вместо модулей CommonJS предназначена следующая настройка, устанавливаемая в файле tsconfig.json
:
{
"compilerOptions": {
"module": "es2020"
}
}
Поле type
файла package.json
: вместо указанной выше настройки можно определить следующее поле в файле package.json
:
"type": "module"
Выражения инстанцирования / Instantiation expressions: позволяют определять параметры типов при ссылке на значения. Это позволяет конкретизировать (narrow) общие типы без создания оберток.
class List<T> {
private list: T[] = [];
get(key: number): T {
return this.list[key];
}
push(value: T): void {
this.list.push(value);
}
}
function makeList<T>(items: T[]): List<T> {
const list = new List<T>();
items.forEach(item => list.push(item));
return list;
}
// Предположим, что мы хотим определить функцию, создающую список
// элементов определенного типа
// Раньше:
// Требовалось создавать функцию-обертку и передавать ей аргумент с указанием типа
function makeStringList(text: string[]) {
return makeList(text);
}
// Сейчас:
// Можно использовать выражение инстанцирования
const makeNumberList = makeList<number>;
extends
и infer
: при выводе переменных типов в условных типах, они могут конкретизироваться/ограничиваться с помощью ключевого слова extends
.
// Предположим, что мы хотим извлекать тип первого элемента массива только в случае,
// если такой элемент является строкой
// Для этого можно применить условные типы
// Раньше:
type FirstIfStringOld<T> =
T extends [infer S, ...unknown[]]
? S extends string ? S : never
: never;
// Вместо 2 вложенных условных типов можно использовать 1
type FirstIfString<T> =
T extends [string, ...unknown[]]
// Извлекаем первый тип из типа `T`
? T[0]
: never;
// Но код все равно выглядит не очень хорошо
// Сейчас:
type FirstIfStringNew<T> =
T extends [infer S extends string, ...unknown[]]
? S
: never;
// Обратите внимание: типизация работает как раньше, но код стал чище
type A = FirstIfStringNew<[string, number, number]>; // string
type B = FirstIfStringNew<["hello", number, number]>; // "hello"
type C = FirstIfStringNew<["hello" | "world", boolean]>; // "hello" | "world"
type D = FirstIfStringNew<[boolean, number, string]>; // never
Опциональные аннотации вариативности для параметров типов: дженерики могут вести себя по-разному при проверке на совпадение (match), например, разрешение наследования выполняется в обратном порядке для геттеров и сеттеров. Это может быть определено в явном виде для ясности.
// Предположим, что у нас имеется интерфейс, расширяющий другой интерфейс
interface Animal {
animalStuff: any;
}
interface Dog extends Animal {
dogStuff: any;
}
// А также общий "геттер" и "сеттер".
type Getter<T> = () => T;
type Setter<T> = (value: T) => void;
// Если мы хотим выяснить, совпадают ли Getter<T1> и Getter<T2> или Setter<T1> и Setter<T2>,
// нам следует учитывать ковариантность (covariance)
function useAnimalGetter(getter: Getter<Animal>) {
getter();
}
// Теперь мы можем передать `Getter` в функцию
useAnimalGetter((() => ({ animalStuff: 0 }) as Animal));
// Это работает
// Что если мы хотим использовать `Getter`, возвращающий `Dog`?
useAnimalGetter((() => ({ animalStuff: 0, dogStuff: 0 }) as Dog));
// Это работает, поскольку `Dog` - это также `Animal`
function useDogGetter(getter: Getter<Dog>) {
getter();
}
// Если мы попытаемся сделать тоже самое для функции `useDogGetter`,
// то получим другое поведение
useDogGetter((() => ({ animalStuff: 0 }) as Animal));
// TypeError: Property 'dogStuff' is missing in type 'Animal' but required in type 'Dog'.
// Это не работает, поскольку ожидается `Dog`, а не просто `Animal`
useDogGetter((() => ({ animalStuff: 0, dogStuff: 0 }) as Dog));
// Однако, это работает
// Можно предположить, что сеттеры работает как геттеры, но это не так
function setAnimalSetter(setter: Setter<Animal>, value: Animal) {
setter(value);
}
// Если мы передадим `Setter` такого же типа, все будет хорошо
setAnimalSetter((value: Animal) => {}, { animalStuff: 0 });
function setDogSetter(setter: Setter<Dog>, value: Dog) {
setter(value);
}
// И здесь
setDogSetter((value: Dog) => {}, { animalStuff: 0, dogStuff: 0 });
// Но если мы передадим `Dog Setter` в функцию `setAnimalSetter`,
// поведение будет противоположным (reversed) `Getter`
setAnimalSetter((value: Dog) => {}, { animalStuff: 0, dogStuff: 0 });
// TypeError: Argument of type '(value: Dog) => void' is not assignable to parameter of type 'Setter<Animal>'.
// Обходной маневр выглядит несколько иначе
setDogSetter((value: Animal) => {}, { animalStuff: 0, dogStuff: 0 });
// Сейчас:
// Не является обязательным, но повышает читаемость кода
type GetterNew<out T> = () => T;
type SetterNew<in T> = (value: T) => void;
Кастомизация разрешения модулей: настройка moduleSuffixes
позволяет указывать кастомные суффиксы файлов (например, .ios
) при работе в специфических окружениях для правильного разрешения импортов.
{
"compilerOptions": {
"moduleSuffixes": [".ios", ".native", ""]
}
}
import * as foo from './foo';
// Сначала проверяется ./foo.ios.ts, затем ./foo.native.ts и, наконец, ./foo.ts
Переход к определению источника / Go to source definition: новый пункт меню в редакторе кода. Он похож на "Перейти к определению" (Go to definition), но "предпочитает" файлы .ts
и .js
вместо определений типов (.d.ts
).
TS4.9
Оператор satisfies
: позволяет проверять совместимость значения с типом без присвоения типа. Это делает вывод типов более точным при сохранении совместимости.
// Раньше:
// Предположим, что у нас есть объект, в котором хранятся разные элементы и их цвета
const obj = {
fireTruck: [255, 0, 0],
bush: '#00ff00',
ocean: [0, 0, 255]
} // { fireTruck: number[]; bush: string; ocean: number[]; }
const rgb1 = obj.fireTruck[0]; // number
const hex = obj.bush; // string
// Допустим, мы хотим ограничить типы значений объекта
// Для этого можно применить утилиту типа `Record`
const oldObj: Record<string, [number, number, number] | string> = {
fireTruck: [255, 0, 0],
bush: '#00ff00',
ocean: [0, 0, 255]
} // Record<string, [number, number, number] | string>
// Но это приводит к потере типизации свойств
const oldRgb1 = oldObj.fireTruck[0]; // string | number
const oldHex = oldObj.bush; // string | number
// Сейчас:
// Оператор `satisfies` позволяет проверять совместимость значения с типом без присвоения типа
const newObj = {
fireTruck: [255, 0, 0],
bush: '#00ff00',
ocean: [0, 0, 255]
} satisfies Record<string, [number, number, number] | string>
// { fireTruck: [number, number, number]; bush: string; ocean: [number, number, number]; }
// Типизация свойств сохраняется
// Более того, массив становится кортежем
const newRgb1 = newObj.fireTruck[0]; // number
const newRgb4 = newObj.fireTruck[3];
// TypeError: Tuple type '[number, number, number]' of length '3' has no element at index '3'.
const newHex = newObj.bush; // string
Новые команды: в редакторе кода появились команды "Удалить неиспользуемые импорты" (Remove unused imports) и "Сортировать импорты" (Sort imports), облегчающие управления импортами.
На этом перевод второй части, посвященной возможностям TS, завершен.
С возможностями TS5, можно ознакомиться здесь.
Надеюсь, вы узнали что-то новое и не зря потратили время.
Happy coding!