Объекты
Тип объекта
Ранее мы рассматривали примитивные типы и массивы. Сейчас мы посмотрим на то, как TypeScript работает с объектами
const a = {a: 10};
Какой тип TypeScript присвоит переменной a
? У этой переменной будет тип { a: number }
. Мы можем явно задать переменной этот тип. Это делается по аналогии с другими типами.
const a: {a: number} = {a: 10}
Если попытаться присвоить переменной с типом {a: number}
объект без поля a
или с полем a
другого типа - TypeScript выдаст ошибку.
const a: {a: number} = {b: 10}; //Error: Type '{ b: number; }' is not assignable to type '{ a: number; }'.
const b: {a: number} = {a: "lalaka"}; //Error: Type 'string' is not assignable to type 'number'.
const c: {a: number} = {a: true}; //Error: Type 'true' is not assignable to type 'number'.
После инициализации объекта в нем нельзя определить поле, которого нет в типе. Также нельзя присвоить полю значения типа, который не совместим с типом поля.
const a = {a: 10, b: "lalaka"};
a.a = 300;
a.a = "malaka"; //Error: Type '"malaka"' is not assignable to type 'number'.
a.d = true; //Error: Property 'd' does not exist on type '{ a: number; b: string; }'.
Отметим, что все правила типизации переменных, которые были рассмотрены ранее, применяются и для полей объекта. Иными словами, поле объекта может быть примитивным типом, другим объектом, типом-литералом, объединением или пересечением типов, массивом, кортежем и так далее.
Проверка типов объектов
В TypeScript тип объекта x
совместим с типом объекта y
, если у y
есть все поля с совместимыми типами, что и у x
.
let x: {a: number, b: string};
const y = {a: 10, b: "lalaka"};
x = y;
При этом тип y
может иметь поля, которых нет в типе x
.
let x: {a: number, b: string};
const y = {a: 10, b: "lalaka", c: true};
x = y;
Важно заметить, что типы полей двух объектов не обязательно должны быть идентичны. Проверка типов совместимости полей работает по тем же правилам, что и присваивание значения переменной.
let x: {a: number, b: string};
const y: {a: 10, b: "lalaka"} = {a: 10, b: "lalaka"};
x = y;
Такой подход к проверке типов называется структурной системой типов или "утиной типизацией" (duck typing).
Type alias
До этого момента все типы были описаны по месту или использовался автовывод типов. Однако, в TypeScript есть возможность объявить тип отдельно и использовать его в нескольких местах.
type MyType = string
Такая конструкция называется type alias. Таким образом происходит инициализация своего рода "переменной" в системе типизации. Во время компиляции в JavaScript type alias-ы в результирующий файл не попадут.
В type alias можно сохранять любые типы, например, результаты выполнений операций над типами (объединения, пересечения и т.д.), примитивные типы, массивы, объекты, литералы и так далее.
type A = string
type B = number | boolean
type C = A | B
const c: C = 10 // type: string | number | boolean
type Q = {
a: A;
b: B;
c: C;
tuple: [A, B, "lalaka", string];
array: number[];
aliasArray: A[];
}
typeof
Иногда возникает потребность сохранить тип какой-то уже инициализированной переменной и использовать этот тип в другом месте. Для этого в TypeScript есть оператор typeof
.
const x = {a: "lalaka", b: 10};
type X = typeof x;
const y: X = {a: "malaka", b: 15} // type: {a: string, b: number}
const z: typeof x = {a: "palaka", b: 30} //type: {a: string, b: number}
Очень важно не путать этот оператор с одноименным оператором из JavaScript. TypeScript поддерживает оба оператора. Это возможно потому, что синтаксически они используются в разных местах: один в местах работы с типами, другой - со значениями.
let x = 17;
type X = typeof x //Работает с типом (ключевое слово type)
const y: /* Работаем с типом (после : перед =) */ typeof x = 15;
const z = typeof x === "string" //Работаем со значением
Интерфейс
Существует еще один способ описать тип объекта - использовать интерфейс.
interface A {
a: number;
b: string:
q: boolean;
}
Для этого необходимо использовать ключевое слово interface
, далее указать название типа и без знака =
указать тип объекта. Чаще для указания типа объекта используется именно интерфейс, а не type alias.
interface A {
a: number;
}
const a: A = {a: 10};
type B = A;
type C = {
a: A;
}
const c: C = {
a: {
a: 10
}
}
Различия между type alias и interface
В type alias можно положить не только объект, а любой тип, включая interface или другой type alias.
interface A {
a: number
}
type B = A;
type C = B;
type D = string | 10 | ["lalaka"] | null | undefined;
type E = C | D;
При объявлении интерфейсов с одинаковым именем тип объекта расширяется
interface A {
a: number;
}
interface A {
b: string;
}
const a: A = { a: 10, b: "lalaka" }
Если же объявить два type alias с одинаковым именем, будет ошибка
type A = { //Error: Duplicate identifier 'A'.
a: number;
}
type A = { //Error: Duplicate identifier 'A'.
b: string;
}
Интерфейсы могут наследоваться друг от друга. Это можно сделать при помощи ключевого слова
extends
. При наследовании интерфейс-потомок имеет все поля интерфейса-родителя и те поля, которые объявлены непосредственно у него самого. Type alias наследоваться не могут.
interface A {
a: number;
}
interface B extends A
{
b: string;
}
interface C extends A
{
c: boolean;
}
const a: A = { a: 10 };
const b: B = { a: 15, b: "lalaka" };
const c: C = { a: 20, c: true };
Наследование позволяет выделить у объектов общие признаки и работать с разными значениями как с общим типом. Например.
interface DomElement {
width: number;
height: number;
}
interface DivElement extends DomElement {
textContent: string;
}
interface ButtonElement extends DomElement {
tabIndex: number;
}
Мы описали интерфейс DomElement
, который определяет размеры элемента. У этого интерфейса 2 наследника DivElement
и ButtonElement
. Оба эти наследника имеют все поля базового типа (а именно width
и height
) и какие-то собственные поля.
const div: DivElement = {textContent: "lalaka", width: 100, height: 15};
const button: ButtonElement = {tabIndex: 0, width: 500, height: 300};
Не смотря на то, что переменные div
и button
имеют типы DivElement
и ButtonElement
соответственно, их всегда можно интерпретировать как DomElement
.
let dom: DomElement = div;
dom = button;
const tree: DomElement[] = [
dom,
div,
button,
div,
button
];
Однако, как только более конкретный тип (наследник) начинает использоваться как базовый тип, все особенности (поля и методы) конкретного типа теряются (с точки зрения типизация - в runtime все есть).
const q: DomElement = div;
q.textContent // Error: Property 'textContent' does not exist on type 'DomElement'.
tree[1].textContent // Error: Property 'textContent' does not exist on type 'DomElement'.
Это далеко не все отличительные особенности интерфейсов и type alias. Далее будут выявляться все больше отличительных особенностей этих конструкций. Однако, уже сейчас можно сделать несколько выводов:
Интерфейсы позволяют типизировать только объекты. Поэтому поддерживают некоторые специфические особенности типизации объектов, такие как наследование и расширение.
Type alias предназначены для хранения абсолютно любого типа. Из-за этого они не поддерживают специфические особенности типизации объектов, также как и специфические особенности типизации других типов (строк, булевых значений и т.д.). Но так как тип объекта - это тоже тип, а type alias - хранилище для любого типа, отсюда и появляется это двоякая возможность задания типа объекта.
Readonly и опциональные поля
В TypeScript при описании типа объекта можно указать, что поле является не изменяемыми. Делается это при помощи модификатора readonly
.
interface A {
readonly a: number;
b: string;
}
Значение этих полей указывается во время инициализации объекта. Присвоить новое значение таким полям нельзя - можно только полностью заменить весь объект.
let a: A;
a = { a: 10, b: "lalaka" }
a.b = "malaka";
a.a = 15 //Error: Cannot assign to 'a' because it is a read-only property.
a = {a: 15, b: "palaka"}
Важно отметить что модификатор readonly
работает только во время проверки типов. После компиляции readonly
(как и все типы) не попадают в результирующие файлы. Поэтому в runtime эти поля можно изменять как угодно.
Часто возникает потребность описать тип объекта, у которого некоторые поля являются опциональными. Для этого в TypeScript можно воспользоваться знаком ?
.
interface A {
a?: number;
b: string;
}
const a1: A = { b: "lalaka" };
const a2: A = { a: 10, b: "malaka"}
Поле может быть и опциональным, и readonly
одновременно. Такое поле можно не указывать во время инициализации объекта, но нельзя менять далее в коде.
Индексаторы
В TypeScript в типе объекта можно описать индексатор.
interface A {
[index: number]: boolean;
}
const a: A = {
[1]: true,
[-1]: false
}
a[3] = true
a[1] = a[1] === a[-1] === a[100500]
Индексатор говорит о том, что у объекта нет конкретных названий полей. Известен лишь тип полей и тип значения поля. Тип полей может быть или string
, или number
. Нельзя использовать литеральные типы или их объединения (по крайней мере в interface-ах и в таком виде). Вместо это нужно объявлять тип с обычными полями.
В одном типе могут быть сразу два индексатора с разным типом полей, но тип значений числового индекса, должен быть подтипом строчного индекса. Это ограничение введено из-за особенностей JavaScript. Когда вы используется числовой индекс [10]
, JavaScript конвертирует значение индекса в строку. Получается, что с точки зрения JavaScript вызов индекса [10]
и ["10"]
- эквивалентны.
interface A {
[index: string]: boolean;
[index: number]: boolean;
}
interface B {
[index: string]: string;
[index: number]: boolean; //Error: Numeric index type 'boolean' is not assignable to string index type 'string'.
}
Пересечение объектов
В прошлой части мы говорили про операцию пересечения типов (&
). Для простых типов эта операция почти не имеет смысла, но для типов объектов она очень важна. Предположим, у нас есть два тип объекта.
interface A {
a: number;
}
interface B {
b: string;
}
Рассмотрим два этих типа с точки зрения множеств. Тип A
является множеством всех объектов, у которых есть поле a
, значение которого принадлежит множеству number
. Тип B
- множество объектов с полем b
, тип которого string
. Из-за того, что TypeScript использует утиную типизацию, то типу A
может подходить объект, который имеет поле a: number
и еще любое количество других полей с любым типом. Точно также для B
.
const q = {a: 10, b: "lalaka"};
const a: A = q;
const b: B = q;
Из этого следует, что существует такие объекты, которые одновременно входят во множество A
и во множество B
. Множество этих объектов можно обозначить как пересечение множества A
и B
- A & B
. С точки зрения типизации типу A & B
можно присвоить любой объект, у которого обязательно должны быть поля a: number
и b: string
, и любое количество других полей.
type Q = A & B;
const a: Q = { a: 10, b: "lalaka" };
const b: Q = { a: 10 }; /* Error:
Type '{ a: number; }' is not assignable to type 'Q'.
Property 'b' is missing in type '{ a: number; }' but required in type 'B'.
*/
const c: Q = { b: "malaka" }; /* Error:
Type '{ b: string; }' is not assignable to type 'Q'.
Property 'a' is missing in type '{ b: string; }' but required in type 'A'.
*/
Взятие тип значения ключа. keyof
В TypeScript из типа объекта по названию ключей можно извлекать типы значений этих ключей.
interface A {
a: number;
b: string;
c: boolean;
}
type Aa = A["a"] //number
type Ab = A["b"] //string
type Ac = A["c"] //boolean
Применяя к типу объекта []
с названием ключа, мы получаем тип значения, который соответствует этому ключу. Если попытаться взять тип значения ключа, которого в объекте нет - TypeScript выдаст ошибку.
type Wrong = A["e"] //Error: Property 'e' does not exist on type 'A'.
Важно, что в []
можно передавать не только один ключ, а объединение названий ключей (например, "a" | "b"
). В результате такого действия мы получим тип, который объединяет типы значений переданных ключей.
type Aab = A["a" | "b"] // number | string
type Aac = A["a" | "c"] // number | boolean
type Aabc = A["a" | "b" | "c"] // number | string | boolean
В TypeScript есть специальный оператор keyof
. Он возвращает объединение названий ключей типа объекта, к которому его применили.
type Keys = keyof A // "a" | "b" | "c"
Так как keyof
возвращает объединение названий, то его результат можно сразу передать в []
, чтобы получить объединение всех типов значений ключей объекта
type Q = A[keyof A] //number | string | boolean
Возможно сейчас трудно представить, какие задачи решаются при помощи keyof
и взятии типов значений полей. Эти инструменты являются частями более сложных механизмов TypeScript, хотя могут быть использованы и сами по себе. Например, в TypeScript из коробки предоставляется интерфейс HTMLElement
. Этот интерфейс является базовым интерфейсом для любого DOM-элемента, который можно получить через document.createElement или каким-то другим способом. Если мы хотим типизировать переменную или поле объекта так, чтобы туда можно было положить значение поля style
DOM-элемента нам достаточно сделать такое действие.
const style: HTMLElement["style"] = {...}
Или мы хотим вынести тип поля объекта, не имея типа объекта.
const a = {
a: 10,
b: "lalaka"
}
type B = (typeof a)["b"] //string
Задания
Пример решения задания #4
Last updated
Was this helpful?