TypeScript в деталях
Спасибо Денису Улесову за помощь в редактировании материала.
Обратите внимание: предполагается, что вы имеете некоторый опыт работы с TypeScript
. Если нет, рекомендую начать с:
T
, K
и V
в дженериках / Generics

T
называется параметром общего типа (generic type parameter). Это заменитель (placeholder) настоящего (actual) типа, передава емого функции.
Суть такая: берем тип, определенный пользователем, и привязываем (chain) его к типу параметра функции и типу возвращаемого функцией значения.

Так что все-таки означает T
? T
означает тип (type). На самом деле, вместо T
можно использовать любое валидное название. Часто в сочетании с T
используются такие общие переменные, как K
, V
, E
и др.
K
представляет тип ключа объекта;V
представляет тип значения объекта;E
представляет тип элемента.

Разумеется, мы не ограничены одним параметром типа - их может быть сколько угодно:

При вызове функции identity
можно явно определить действительный тип параметра типа. Или можно позволить TypeScript
самостоятельно сделать вывод относительного него:

Утилиты типа / Utility types
Утилиты типа (utility types) позволяют легко конвертировать, извлекать, исключать типы, получать параметры типов и типы значений, возвращаемых функциями.
1. Partial<Type>
Данная утилита делает все свойства Type
опциональными (необязательными):

/**
* Make all properties in T optional.
* typescript/lib/lib.es5.d.ts
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};

2. Required<Type>
Данная утилита делает все свойства Type
обязательными (она является противоположностью утилиты Partial
):

/**
* Make all properties in T required.
* typescript/lib/lib.es5.d.ts
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};

3. Readonly<Type>
Данная утилита делает все свойства Type
доступными только для чтения (readonly
). Такие свойства являются иммутабельными (их значения нельзя изменять):

/**
* Make all properties in T readonly.
* typescript/lib/lib.es5.d.ts
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

4. Record<Keys, Type>
Данная утилита создает новый объектный тип (object type), ключами которого являются Keys
, а значениями свойств - Type
. Эта утилита может использоваться для сопоставления свойств одного типа с другим типом:

/**
* Construct a type with a set of properties K of type T.
* typescript/lib/lib.es5.d.ts
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
5. Exclude<UnionType, ExcludedMembers>
Данная утилита создает новый тип посредством исключения из UnionType
всех членов объединения, которые могут быть присвоены (assignable) ExcludedMembers
:

/**
* Exclude from T those types that are assignable to U.
* typescript/lib/lib.es5.d.ts
*/
type Exclude<T, U> = T extends U ? never : T;

6. Extract<Type, Union>
Данная утилита создает новый тип посредством извлечения из Type
всех членов объединения, которые могут быть присвоены Union
:

/**
* Extract from T those types that are assignable to U.
* typescript/lib/lib.es5.d.ts
*/
type Extract<T, U> = T extends U ? T : never;

7. Pick<Type, Keys>
Данная утилита создает новый тип посредством извлечения из Type
набора (множества) свойств Keys
(Keys
- строковый литерал или их объединение):

/**
* From T, pick a set of properties whose keys are in the union K.
* typescript/lib/lib.es5.d.ts
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

8. Omit<Type, Keys>
Данная утилита создает новый тип посредством исключения из Type
набора свойств Keys
(Keys
- строковый литерал или их объединение) (она является противоположностью утилиты Pick
):

/**
* Construct a type with the properties of T except for those
* in type K.
* typescript/lib/lib.es5.d.ts
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

9. NonNullable<Type>
Данная утилита создает новый тип посредством исключения из Type
значений null
и undefined
:

/**
* Exclude null and undefined from T.
* typescript/lib/lib.es5.d.ts
*/
type NonNullable<T> = T extends null | undefined ? never : T;
10. Parameters<Type>
Данная утилита создает кортеж (tuple) из типов параметров функции Type
:

/**
* Obtain the parameters of a function type in a tuple.
* typescript/lib/lib.es5.d.ts
*/
type Parameters<T extends (...args: any) => any> = T extends
(...args: infer P) => any ? P : never;
11. ReturnType<Type>
Данная утилита извлекает тип значения, возвращаемого функцией Type
:

/**
* Obtain the return type of a function type.
* typescript/lib/lib.es5.d.ts
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
12. Uppercase<StringType>
Данная утилита конвертирует строковый литеральный тип в верхний регистр:

13. Lowercase<StringType>
Данная утилита конвертирует строковый литеральный тип в нижний регистр:

14. Capitalize<StringType>
Данная утилита конвертирует первый символ строкового литерального типа в верхний регистр:

15. Uncapitalize<StringType>
Данная утилита конвертирует первый символ строкового литерального типа в нижний регистр:

Кроме описанных выше, существует еще несколько встроенных утилит типа:
ConstructorParameters<Type>
: создает кортеж или массив из конструктора функции (речь во всех случаях идет о типах). Результатом является кортеж всех параметров типа (или типnever
, еслиType
не является функцией);InstanceType<Type>
: создает тип, состоящий из типа экземпляра конструктора функции типаType
:ThisParameterType<Type>
: извлекает тип из параметраthis
функции. Если функция не имеет такого параметра, возвращаетсяunknown
.
Классы / Classes
В объектно-ориентированных языках программирования класс - это шаблон (blueprint - проект, схема), оп исывающий свойства и методы, которые являются общими для всех объектов, создаваемых с помощью класса.
1. Свойства и методы
1.1. Свойства экземпляров и статические свойства
В TS
, как и в JS
, класс определяется с помощью ключевого слова class
:
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
В приведенном примере определяется класс User
с одним свойством экземпляров name
. В действительности, класс - это синтаксический сахар для функции-конструктора. Если установить результат компиляции в ES5
, то будет сгенерирован следующий код:
"use strict";
var User = /** @class */ (function () {
function User(name) {
this.name = name;
}
return User;
}());
Кроме свойств экземпляров, в классе могут определяться статические свойства. Такие свойства определяются с помощью ключевого слова static
:
class User {
static cid: string = "eft";
name: string;
constructor(name: string) {
this.name = name;
}
}
В чем разница между свойствами экземпляров и статическими свойствами? Посмотрим на компилируемый код:
"use strict";
var User = /** @class */ (function () {
function User(name) {
this.name = name;
}
User.cid = "eft";
return User;
}());
Как видим, свойства экземпляров определяются в экземпляре класса, а статические свойства - в его конструкторе.
1.2. Методы экземпляров и статические методы
Кроме свойств, в классе могут определяться методы экземпляров и статические методы:
class User {
static cid: string = "eft";
name: string;
constructor(name: string) {
this.name = name;
}
static printCid() {
console.log(User.cid);
}
send(msg: string) {
console.log(`${this.name} send a message: ${msg}`);
}
}
В чем разница между методами экземпляров и статическими методами? Посмотрим на компилируемый код:
"use strict";
var User = /** @class */ (function () {
function User(name) {
this.name = name;
}
User.printCid = function () {
console.log(User.cid);
};
User.prototype.send = function (msg) {
console.log("".concat(this.name, " send a message: ").concat(msg));
};
User.cid = "eft";
return User;
}());
Как видим, методы экземпляров добавляются в прототип конструктора, а статические методы в сам конструктор.
2. Аксессоры
В классе могут определяться так называемые аксессоры (accessors). Аксессоры, которые делятся на геттеры (getters) и сеттеры (setters) могут использоваться, например, для инкапсуляции данных или их верификации:
class User {
private _age: number = 0;
get age(): number {
return this._age;
}
set age(value: number) {
if (value > 0 && value <= 120) {
this._age = value;
} else {
throw new Error("The set age value is invalid!");
}
}
}
3. Наследование
Наследование (inheritance) - это иерархическая модель для связывания классов между собой. Наследование - это возможность класса наследовать функционал другого класса и расширять его новым функционалом. Наследование - это наиболее распространенный вид отношений между классами, между классами и интерфейсами, а также между интерфейсами. Наследование облегчает повторное использование кода.
Наследование реализуется с помощью ключевого слова extends
. Расширяемый класс называется базо вым (base), а расширяющий - производным (derived). Производный класс содержит все свойства и методы базового и может определять дополнительные члены.
3.1. Базовый класс
class Person {
constructor(public name: string) {}
public say(words: string) :void {
console.log(`${this.name} says:${words}`);
}
}
3.2. Производный класс
class Developer extends Person {
constructor(name: string) {
super(name);
this.say("Learn TypeScript")
}
}
const bytefer = new Developer("Bytefer");
// "Bytefer says:Learn TypeScript"
Класс Developer
расширяет (extends) класс Person
. Следует отметить, что класс может расширять только один класс (множественное наследование в TS
, как и в JS
, запрещено):

Однако мы вполне можем реализовывать (implements) несколько интерфейсов:
interface CanSay {
say(words: string) :void
}
interface CanWalk {
walk(): void;
}
class Person implements CanSay, CanWalk {
constructor(public name: string) {}
public say(words: string) :void {
console.log(`${this.name} says:${words}`);
}
public walk(): void {
console.log(`${this.name} walk with feet`);
}
}
Рассмотренные классы являются конкретными (concrete). В TS
также существуют абстрактные (abstract) классы.
4. Абстрактные классы
Классы, поля и методы могут быть абстрактными. Класс, определенный с помощью ключевого слова abstract
, является абстрактным. Абстрактные классы не позволяют создавать объекты (другими словами, они не могут инстанцироваться (instantiate) напрямую):

Абстрактный класс - это своего рода проект класса. Подклассы (subclasses) абстрактного класса должны реализовывать всех его абстрактных членов:
class Developer extends Person {
constructor(name: string) {
super(name);
}
say(words: string): void {
console.log(`${this.name} says ${words}`);
}
}
const bytefer = new Developer("Bytefer");
bytefer.say("I love TS!"); // Bytefer says I love TS!
5. Видимость членов
В TS
для управления видимостью (visibility) свойств и методов класса применяются ключевые слова public
, protected
и private
. Видимость означает возможность доступа к членам за пределами класса, в котором они определяются.
5.1. public
Дефолтной видимостью членов класса является public
. Такие члены доступны за пределами класса без каких-либо ограничений:
class Person {
constructor(public name: string) {}
public say(words: string) :void {
console.log(`${this.name} says:${words}`);
}
}
5.2. protected
Такие члены являются защищенными. Это означает, что они доступны только в определяющем их классе, а также в производных от него классах:

class Developer extends Person {
constructor(name: string) {
super(name);
console.log(`Base Class:${this.getClassName()}`);
}
}
const bytefer = new Developer("Bytefer"); // "Base Class:Person"
5.3. private
Такие члены являются частными (приватными). Это означает, что они доступны только в определяющем их классе:

Обратите внимание: private
не делает членов по-настоящему закрытыми. Это всего лишь соглашение (как префикс _
в JS
). Посмотрим на компилируемый код:
"use strict";
var Person = /** @class */ (function () {
function Person(id, name) {
this.id = id;
this.name = name;
}
return Person;
}());
var p1 = new Person(28, "bytefer");
5.4. Частные поля
Реальные закрытые поля поддерживаются в TS
, начиная с версии 3.8
(а в JS
- с прошлого года):

Посмотрим на компилируемый код:
"use strict";
var __classPrivateFieldSet = // игнорировать соответствующий код;
var _Person_name;
class Person {
constructor(name) {
_Person_name.set(this, void 0);
__classPrivateFieldSet(this, _Person_name, name, "f");
}
}
_Person_name = new WeakMap();
const bytefer = new Person("Bytefer");
Отличия между частными и обычными полями могут быть сведены к следующему:
- закрытые поля определяются с помощью префикса
#
; - областью видимости приватного поля является определяющий его класс;
- в отношении частных полей не могут применяться модификаторы доступа (
public
и др.); - приватные поля н едоступны за пределами определяющего их класса.
6. Выражение класса
Выражение класса (class expression) - это синтаксис, используемый дял определения классов. Как и функциональные выражения, выражения класса могут быть именованными и анонимными. В случае с именованными выражениями, название доступно только в теле класса.
Синтаксис выражений класса (квадратные скобки означают опциональность):
const MyClass = class [className] [extends] {
// тело класса
};
Пример определения класса Point
:
const Point = class {
constructor(public x: number, public y: number) {}
public length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
}
const p = new Point(3, 4);
console.log(p.length()); // 5
При определении класса с помощью выражения также можно использовать ключевое слово extends
.
7. Общий класс
Для определения общего (generic) класса используется синтаксис <T, ...>
(параметры типа) после названия класса:
class Person<T> {
constructor(
public cid: T,
public name: string
) {}
}
const p1 = new Person<number>(28, "Lolo");
const p2 = new Person<string>("eft", "Bytefer");
Рассмотрим пример инстанцирования p1
:
- при создании объекта
Person
передается типnumber
и параметры конструктора; - в классе
Person
значение переменной типаT
становится числом; - наконец, параметр типа свойства
cid
в конструкторе также становится числом.
Случаи использования дженериков:
- интерфейс, функция или класс работают с несколькими типами данных;
- в интерфейсе, функции или классе тип данных используется в нескольких местах.
8. Сигнатура конструктора
При определении интерфейса для описания конструктора может использоваться ключевое слово new
:
interface Point {
new (x: number, y: number): Point;
}
new (x: number, y: number)
называется сигнатурой конструктора (construct signature). Она имеет следующий синтаксис:
ConstructSignature: new TypeParametersopt ( ParameterListopt ) TypeAnnotationopt
TypeParametersopt
, ParameterListopt
и TypeAnnotationopt
- это опциональный параметр типа, опциональный список параметров и опциональная аннотация типов, соответственно. Как применяется сигнатура конструктора? Рассмотрим пример:
interface Point {
new (x: number, y: number): Point;
x: number;
y: number;
}
class Point2D implements Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const point: Point = new Point2D(1, 2); // Error
Сообщение об ошибке выглядит так:
Type 'Point2D' is not assignable to type 'Point'.
Type 'Point2D' provides no match for the signature 'new (x: number, y: number): Point'.ts(2322)
Для решения проблемы определен ный ранее интерфейс Point
нужно немного отрефакторить:
interface Point {
x: number;
y: number;
}
interface PointConstructor {
new (x: number, y: number): Point;
}
Далее определяем фабричную функцию newPoint
, которая используется для создания объекта Point
, соответствующего конструктору входящего типа PointConstructor
:
class Point2D implements Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
function newPoint(
pointConstructor: PointConstructor,
x: number,
y: number
): Point {
return new pointConstructor(x, y);
}
const point: Point = newPoint(Point2D, 3, 4);
9. Абстрактная сигнатура конструктора
Абстрактная сигнатура конструктора была представлена в TS 4.2
для решения таких проблем, как:
type Constructor = new (...args: any[]) => any;
abstract class Shape {
abstract getArea(): number;
}
const Ctor: Constructor = Shape; // Error
// Type 'typeof Shape' is not assignable to type 'Constructor'.
// Cannot assign an abstract constructor type to a non-abstract
// constructor type.ts(2322)
Как видим, тип абстрактного конструктора не может присваиваться типу реального конструктора. Для решения данной проблемы следует использовать абстрактную сигнатуру конструктора:
type AbstractConstructor = abstract new (...args: any[]) => any;
abstract class Shape {
abstract getArea(): number;
}
const Ctor: AbstractConstructor = Shape; // Ok
Далее определяем функцию makeSubclassWithArea
для создания подклассов класса Shape
:
function makeSubclassWithArea(Ctor: AbstractConstructor) {
return class extends Ctor {
#sideLength: number;
constructor(sideLength: number) {
super();
this.#sideLength = sideLength;
}
getArea() {
return this.#sideLength ** 2;
}
};
}
const Square = makeSubclassWithArea(Shape);
Следует отметить, что типы реальных конструкторов типам абстрактных конструкторов присваивать можно:
abstract class Shape {
abstract getArea(): number;
}
class Square extends Shape {
#sideLength: number;
constructor(sideLength: number) {
super();
this.#sideLength = sideLength;
}
getArea() {
return this.#sideLength ** 2;
}
}
const Ctor: AbstractConstructor = Shape; // Ok
const Ctor1: AbstractConstructor = Square; // Ok
В заключение кратко рассмотрим разницу между типом class
и типом typeof class
.
10. Тип class
и тип typeof class

На основе результатов приведенного примера можно сделать следующие выводы:
- при использовании класса
Person
в качестве типа значение переменной ограничивается экземпляром этого класса; - при использовании
typeof Person
в качестве типа значение переменной ограничивается статическими свойствами и методами данного класса.
Следует отметить, что в TS
используется система структурированных типов (structured type system), которая отличается от системы номинальных типов (nominal type system), применяемой в Java/C++
, поэтому следующий код в TS
будет работать без каких-либо проблем:
class Person {
constructor(public name: string) {}
}
class SuperMan {
constructor(public name: string) {}
}
const s1: SuperMan = new Person("Bytefer"); // Ok
Связанные типы / Mapped types
Приходилось ли вам использовать вспомогательные типы Partial
, Required
, Readonly
и Pick
?

Интересно, как они реализованы?
Регистрация пользователей является распространенной задачей в веб-разработке. Определим тип User
, в котором все ключи являются обязательными:
type User = {
name: string
password: string
address: string
phone: string
}
Как правило, зарегистрированные пользователи могут модифицировать некоторые данные о себе. Определим новый тип PartialUser
, в котором все ключи являются опциональными:
type PartialUser = {
name?: string
password?: string
address?: string
phone?: string
}
В отдельных случаях требуется, чтобы все ключи были доступными только для чтения. Определим новый тип ReadonlyUser
:
type ReadonlyUser = {
readonly name: string
readonly password: string
readonly address: string
readonly phone: string
}
Получаем много дублирующегося кода:

Как можно уменьшить его количество? Ответ - использовать сопоставленные типы, которые являются общими типами (generic types), позволяющими связывать тип исходного объекта с типом нового объекта.

Синтаксис связанных типов:

P in K
можно сравнить с инструкцией for..in
в JavaScript
, она используется для перебора всех ключей типа K
. Тип переменной T
- это любой тип, валидный с точки зрения TS
.

В процессе связывания типов могут использоваться дополнительные модификаторы, такие как readonly
и ?
. Соответствующие модификаторы добавляются и удаляются с помощью символов +
и -
. По умолчанию модификатор добавляется.
Синтаксис основных связанных типов:
{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }
Несколько примеров:

Переопределим тип PartialUser
с помощью связанного типа:
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
type PartialUser = MyPartial<User>
MyPartial
используется для сопоставления типов User
и PartialUser
. Оператор keyof
возвращает все ключи типа в виде объединения (union type). Тип переменной P
меняется на каждой итерации. T[P]
используется для получения типа значения, соответствующего атрибуту типа объекта.
Демонстрация потока выполнения MyPartial
:

TS 4.1
позволяет повторно связывать ключи связанных типов с помощью ключевого слова as
. Синтаксис выглядит так:
type MappedTypeWithNewKeys<T> = {
[K in keyof T as NewKeyType]: T[K]
// ^^^^^^^^^^^^^
}
Тип NewKeyType
должен быть подтипом объединения string | number | symbol
. as
позволяет определить вспомогательный тип, генерирующий соответствующие геттеры для объектного типа:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
interface Person {
name: string
age: number
location: string
}
type LazyPerson = Getters<Person>
// {
// getName: () => string
// getAge: () => number
// getLocation: () => string
// }

Поскольку тип, возвращаемый keyof T
может содержать тип symbol
, а вспомогательный тип Capitalize
требует, чтобы обрабатываемый тип был подтипом string
, фильтрация типов с помощью оператора &
в данном случае является обязательной.
Повторно связываемые ключи можно фильтровать путем возвращения типа never
:
// Удаляем свойство 'kind'
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, 'kind'>]: T[K]
}
interface Circle {
kind: 'circle'
radius: number
}
type KindlessCircle = RemoveKindField<Circle>
// type KindlessCircle = {
// radius: number
// }
Условные типы / Conditional types
Приходилось ли вам использовать утилиты типов Exclude
, Extract
, NonNullable
, Parameters
и ReturnType
?
Все эти утилиты основаны на условных типах (conditional types):

Здесь представлена лишь часть процесса
Краткая справка:

Названные утилиты используются для следующих целей:
Exclude
- генерирует новый тип посредством исключения изUnionType
всех членов объединения, указанных вExcludedMembers
;Extract
- генерирует новый тип посредством извлечения изType
всех членов объединения, указанных вUnion
;NonNullable
- генерирует новый тип посредством исключенияnull
иundefined
изType
;Parameters
- генерирует новый кортеж (tuple) из типов параметров функцииType
;ReturnType
- генерирует новый тип, содержащий тип значения, возвращаемого функциейType
.
Примеры использования этих утилит:

Синтаксис условных типов:
T extends U ? X : Y
T
, U
, X
и Y
- заменители типов (см. выше). Сигнатуру можно понимать следующим образом: если T
может быть присвоен U
, возвращается тип X
, иначе возвращается тип Y
. Это чем-то напоминает тернарный оператор в JavaScript
.

Как условные типы используются? Рассмотрим пример:
type IsString<T> = T extends string ? true : false;
type I0 = IsString<number>; // false
type I1 = IsString<"abc">; // true
type I2 = IsString<any>; // boolean
type I3 = IsString<never>; // never

Утилита IsString
позволяет определять, является ли действительный тип, переданный в качестве параметра типа, строковым типом. В дополнение к этому, с помощью условных типов и условных цепочек (conditional chain) можно определять несколько типов за один раз:

Условная цепочка похожа на тернарные выражения в JS
:

Вопрос: что будет, если передать TypeName
объединение (union)?
// "string" | "function"
type T10 = TypeName<string | (() => void)>;
// "string" | "object" | "undefined"
type T11 = TypeName<string | string[] | undefined>;

Почему типы T10
и T11
возвращают объединения? Это объясняется тем, что TypeName
- это распределенный (distributed) условный тип. Условный тип называется распределенным, если проверяемый тип является "голым" (naked), т. е. не обернут в массив, кортеж, промис и т. д.

В случае с распределенными условными типами, когда проверяемый тип является объединением, оно разбивается на несколько веток в процессе выполнения операции:
T extends U ? X : Y
T => A | B | C
A | B | C extends U ? X : Y =>
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

Рассмотрим пример:

Если параметр типа обернут в условный тип, он не будет распределенным, поэтому процесс не разбивается на отдельные ветки.
Рассмотрим поток выполнения (execution flow) встроенной утилиты Exclude
:
type Exclude<T, U> = T extends U ? never : T;
type T4 = Exclude<"a" | "b" | "c", "a" | "b">
("a" extends "a" | "b" ? never : "a") // => never
| ("b" extends "a" | "b" ? never : "b") // => never
| ("c" extends "a" | "b" ? never : "c") // => "c"
never | never | "c" // => "c"

Пример реализации утилиты с помощью условных и связанных (mapped, см. Заметка о Mapped Types и других полезных возможностях современного TypeScript) типов:


type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
interface User {
id: number;
name: string;
age: number;
updateName(newName: string): void;
}
type T5 = FunctionPropertyNames<User>; // "updateName"
type T6 = FunctionProperties<User>; // { updateName: (newName: string) => void; }
type T7 = NonFunctionPropertyNames<User>; // "id" | "name" | "age"
type T8 = NonFunctionProperties<User>; // { id: number; name: string; age: number; }
Данные утилиты позволяют легко извлекать атрибуты функциональных и нефункцио нальных типов, а также связанные с ними объектные типы из типа User
.
Пересечения / Intersection types
Тип можно понимать как коллекцию или набор значений. Например, тип number
можно считать множеством (set) всех чисел. 1.0, 68 принадлежат этому множеству, а "bytefer" нет, поскольку типом "bytefer" является string
.
То же самое справедливо и в отношении объектных типов, которые можно понимать как коллекции объектов. Например, тип Point
в приведенном ниже примере представляет множество объектов со свойствами x
и y
, значениями которых являются числа, а тип Named
представляет множество объектов со свойством name
, значением которого является строка:
interface Point {
x: number;
y: number;
}
interface Named {
name: string;
}

Согласно теории множеств (set theory), множество, содержащее элементы, принадлежащие как множеству A
, так и множеству B
, называется пересечением (intersection) множеств A
и B
:

При пересечении Point
и Named
создается новый тип. Объект нового типа принадлежит как Point
, так и Named
.
TS
предоставляет оператор &
для реализации операции пересечения нескольких типов. Результирующий новый тип называется пересечением (intersection type).
Правила применения оператора &
:
- идентичность (identity): выражение
A & A
эквивалентноA
; - коммутативность (commutativity):
A & B
эквивалентноB & A
(за исключением сигнатур вызова и конструктора, см. ниже); - ассоциативность (associativity):
(A & B) & C
эквивалентноA & (B & C)
; - коллапсирование супертипа (supertype collapsing):
A & B
эквивалентноA
, еслиB
является супертипомA
.

Типы any
и never
являются особенными. Не считая типа never
, пересечением любого типа с any
является any
.
Рассмотрим пересечение типов Point
и Named
:

Новый тип NamedPoint
содержит свойства x
, y
и name
. Но что произойдет при пересечении объектов, содержащих одинаковые свойства со значениями разных типов?
interface X {
c: string;
d: string;
}
interface Y {
c: number;
e: string
}
type XY = X & Y;
type YX = Y & X;
В приведенном примере интерфейсы X
и Y
содержат свойство c
, но значения этого свойства имеют разные типы. Может ли в данном случае значение атрибута c
в типах XY
и YX
быть строкой или числом?

Почему типом значения свойства c
является never
? Дело в том, что значение c
должно быть одновременно и строкой, и числом (string & number
). Но такого типа не существует, поэтому типом значения c
становится never
.
Что произойдет в аналогичном случае с непримитивными значениями? Рассмотрим пример:

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

При вызове f(1, "bytefer")
возникает ошибка:
No overload matches this call.
Overload 1 of 2, '(a: string, b: string): void', gave the following error.
Argument of type 'number' is not assignable to parameter of type 'string'.
Overload 2 of 2, '(a: number, b: number): void', gave the following error.
Argument of type 'string' is not assignable to parameter of type 'number'.
В данном случае компилятор TS
обращается к перегрузкам функции (function overloading) для выполнения операции пересечения. Для решения проблемы можно определить новый тип функции F3
со следующей сигнатурой:

С помощью пересечения можно реализовать некоторые полезные утилиты типа (utility types). Например, реализуем утилиту PartialByKeys
, которая делает типы значений указанных ключей объекта опциональными:

Аналогичным способом можно реализовать утилиту RequiredByKeys
.
Шаблонные литеральные типы / Template literal types
При разработке веб-страниц мы часто используем компоненты Tooltip
или Popover
для отображения каких-либо сообщений. Для удовлетворения различных сценариев эти компоненты должны позволять устанавливать позицию сообщения, например, top
, bottom
, left
, right
и т.д.

Соответствующий тип строковых литералов (string literals) можно определить следующим образом:
type Side = 'top' | 'right' | 'bottom' | 'left';
const side: Side = "rigth"; // Error
// Type '"rigth"' is not assignable to type 'Side'.
// Did you mean '"right"'?
Для многих сценариев этого будет достаточно. Но что если мы хотим расширить список доступных позиций? Например, что если сообщение должно отображаться в верхнем правом углу?

Определим тип Placement
посредством расширения типа Side
:
type Placement = Side
| "left-start" | "left-end"
| "right-start" | "right-end"
| "top-start" | "top-end"
| "bottom-start" | "bottom-end"
Глядя на эти строковые литералы, нетрудно заметить дублирующийся код, такой как -start
и -end
. Кроме того, при определении большого количества литералов легко допустить очепятку.
Существует ли лучший способ решения данной задачи? В TS 4.1
были представлены шаблонные литеральные типы (template literal types), позволяющие делать так:
type Alignment = 'start' | 'end';
type Side = 'top' | 'right' | 'bottom' | 'left';
type AlignedPlacement = `${Side}-${Alignment}`;
type Placement = Side | AlignedPlacement;
Как и шаблонные строки (template strings/literals) в JS
, шаблонные типы заключаются в обратные кавычки (``) и могут содержать заменители (placeholders) в форме ${T}
. Передаваемый тип может быть string
, number
, boolean
или bigint
.

Шаблонные типы позволяют объединять (concatenate) строковые литералы и конвертировать литералы непримитивных типов в соответствующие строковые литералы. Вот парочка примеров:

type EventName<T extends string> = `${T}Changed`;
type Concat<S1 extends string, S2 extends string> = `${S1}-${S2}`;
type ToString<T extends string | number | boolean | bigint> = `${T}`;
type T0 = EventName<"foo">; // 'fooChanged'
type T1 = Concat<"Hello", "World">; // 'Hello-World'
type T2 = ToString<"bytefer" | 666 | true | -1234n>;
// "bytefer" | "true" | "666" | "-1234"
Вопрос: каким будет результат, если тип, переданный в утилиту EventName
или Concat
будет объединением? Давайте проверим:

type T3 = EventName<"foo" | "bar" | "baz">;
// "fooChanged" | "barChanged" | "bazChanged"
type T4 = Concat<"top" | "bottom", "left" | "right">;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"
Почему генерируется такой тип? Это объясняется тем, что в случае шаблонных типов объединения в заменителях распределяются по шаблону:
`[${A|B|C}]` => `[${A}]` | `[${B}]` | `[${C}]`

А в случае с несколькими замени телями, как в утилите Concat
, объединения разрешаются в векторное произведение (cross product):
`${A|B}-${C|D}` => `${A}-${C}` | `${A}-${D}` | `${B}-${C}` | `${B}-${D}`

Работая с шаблонными типами, мы также можем применять встроенные утилиты типов для работы со строками, такие как Uppercase
, Lowercase
, Capitalize
и Uncapitalize
:
type GetterName<T extends string> = `get${Capitalize<T>}`;
type Cases<T extends string> = `${Uppercase<T>} ${Lowercase<T>} ${Capitalize<T>} ${Uncapitalize<T>}`;
type T5 = GetterName<'name'>; // "getName"
type T6 = Cases<'ts'>; // "TS ts Ts ts"
Возможности шаблонных типов являются очень мощными. В сочетании с условными типами и ключевым словом infer
можно реализовать, например, такую утилиту вывода типа (type inference):
type InferSide<T> = T extends `${infer R}-${Alignment}` ? R : T;
type T7 = InferSide<"left-start">; // "left"
type T8 = InferSide<"right-end">; // "right"
TS 4.1
также позволяет использовать оговорку as
для переименования ключей при сопоставлении типов:

Тип NewKeyType
должен быть подтипом объединения string | number | symbol
. В процессе переименования ключей посредством шаблонных типов можно реализовать некоторые полезные утилиты.
Например, определим утилиту Getters
для генерации типов геттеров для соответствующего объекта:

В приведенном примере поскольку тип, возвращаемый keyof T
, может содержать тип symbol
, а утилита Capitilize
требует, чтобы обрабатываемый тип был подтипом строки, необходимо выполнить фильтрацию типов с помощью оператора &
.
Попробует реализовать более сложную утилиту. Например, для извлечения типов из объекта с произвольными вложенными свойствами:

type PropType<T, Path extends string> = string extends Path
? unknown
: Path extends keyof T
? T[Path]
: Path extends `${infer K}.${infer R}`
? K extends keyof T
? PropType<T[K], R>
: unknown
: unknown;
// см. ниже
declare function getPropValue<T, P extends string>(
obj: T,
path: P
): PropType<T, P>;