
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!
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩

