> For the complete documentation index, see [llms.txt](https://race-timo.gitbook.io/typescript/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://race-timo.gitbook.io/typescript/objects.md).

# Объекты

## Тип объекта

Ранее мы рассматривали примитивные типы и массивы. Сейчас мы посмотрим на то, как TypeScript работает с объектами

```typescript
const a = {a: 10};
```

Какой тип TypeScript присвоит переменной `a`? У этой переменной будет тип `{ a: number }`. Мы можем явно задать переменной этот тип. Это делается по аналогии с другими типами.

```typescript
const a: {a: number} = {a: 10}
```

Если попытаться присвоить переменной с типом `{a: number}` объект без поля `a` или с полем `a` другого типа - TypeScript выдаст ошибку.

```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'.
```

После инициализации объекта в нем нельзя определить поле, которого нет в типе. Также нельзя присвоить полю значения типа, который не совместим с типом поля.

```typescript
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`.

```typescript
let x: {a: number, b: string};
const y = {a: 10, b: "lalaka"};
x = y;
```

При этом тип `y` может иметь поля, которых нет в типе `x`.

```typescript
let x: {a: number, b: string};
const y = {a: 10, b: "lalaka", c: true};
x = y;
```

Важно заметить, что типы полей двух объектов не обязательно должны быть идентичны. Проверка типов совместимости полей работает по тем же правилам, что и присваивание значения переменной.

```typescript
let x: {a: number, b: string};
const y: {a: 10, b: "lalaka"} = {a: 10, b: "lalaka"};
x = y;
```

Такой подход к проверке типов называется [структурной системой типов](https://en.wikipedia.org/wiki/Structural_type_system) или "утиной типизацией" ([duck typing](https://en.wikipedia.org/wiki/Duck_typing)).

## Type alias

До этого момента все типы были описаны по месту или использовался автовывод типов. Однако, в TypeScript есть возможность объявить тип отдельно и использовать его в нескольких местах.

```typescript
type MyType = string
```

Такая конструкция называется *type alias*. Таким образом происходит инициализация своего рода "переменной" в системе типизации. Во время компиляции в JavaScript type alias-ы в результирующий файл не попадут.

В type alias можно сохранять любые типы, например, результаты выполнений операций над типами (объединения, пересечения и т.д.), примитивные типы, массивы, объекты, литералы и так далее.

```typescript
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`.

```typescript
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 поддерживает оба оператора. Это возможно потому, что синтаксически они используются в разных местах: один в местах работы с типами, другой - со значениями.

```typescript
let x = 17;
type X = typeof x //Работает с типом (ключевое слово type)
const y: /* Работаем с типом (после : перед =) */ typeof x = 15;

const z = typeof x === "string" //Работаем со значением
```

## Интерфейс

Существует еще один способ описать тип объекта - использовать интерфейс.

```typescript
interface A {
    a: number;
    b: string:
    q: boolean;
}
```

Для этого необходимо использовать ключевое слово `interface`, далее указать название типа и **без знака `=`** указать тип объекта. Чаще для указания типа объекта используется именно интерфейс, а не type alias.

```typescript
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.

```typescript
interface A {
 a: number
}
type B = A; 
type C = B; 
type D = string | 10 | ["lalaka"] | null | undefined;
type E = C | D;
```

* При объявлении интерфейсов с одинаковым именем тип объекта расширяется

```typescript
interface A {
    a: number;
}

interface A {
    b: string;
}

const a: A = { a: 10, b: "lalaka" }
```

Если же объявить два type alias с одинаковым именем, будет ошибка

```typescript
type A = { //Error: Duplicate identifier 'A'.
    a: number;
}

type A = { //Error: Duplicate identifier 'A'.
    b: string;
}
```

* Интерфейсы могут наследоваться друг от друга. Это можно сделать при помощи ключевого слова `extends`. При наследовании интерфейс-потомок имеет все поля интерфейса-родителя и те поля, которые объявлены непосредственно у него самого. Type alias наследоваться не могут.

```typescript
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 };
```

Наследование позволяет выделить у объектов общие признаки и работать с разными значениями как с общим типом. Например.

```typescript
interface DomElement {
    width: number;
    height: number;
}

interface DivElement extends DomElement {
    textContent: string;
}

interface ButtonElement extends DomElement {
    tabIndex: number;
}
```

Мы описали интерфейс `DomElement`, который определяет размеры элемента. У этого интерфейса 2 наследника `DivElement` и `ButtonElement`. Оба эти наследника имеют все поля базового типа (а именно `width` и `height`) и какие-то собственные поля.

```typescript
const div: DivElement = {textContent: "lalaka", width: 100, height: 15};
const button: ButtonElement = {tabIndex: 0, width: 500, height: 300};
```

Не смотря на то, что переменные `div` и `button` имеют типы `DivElement` и `ButtonElement` соответственно, их всегда можно интерпретировать как `DomElement`.

```typescript
let dom: DomElement = div;
dom = button;

const tree: DomElement[] = [
    dom,
    div,
    button,
    div,
    button
];
```

Однако, как только более конкретный тип (наследник) начинает использоваться как базовый тип, все особенности (поля и методы) конкретного типа теряются (с точки зрения типизация - в runtime все есть).

```typescript
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`.

```typescript
interface A {
    readonly a: number;
    b: string;
}
```

Значение этих полей указывается во время инициализации объекта. Присвоить новое значение таким полям нельзя - можно только полностью заменить весь объект.

```typescript
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 можно воспользоваться знаком `?`.

```typescript
interface A {
    a?: number;
    b: string;
}

const a1: A = { b: "lalaka" };
const a2: A = { a: 10, b: "malaka"}
```

Поле может быть и опциональным, и `readonly` одновременно. Такое поле можно не указывать во время инициализации объекта, но нельзя менять далее в коде.

## Индексаторы

В TypeScript в типе объекта можно описать индексатор.

```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"]` - эквивалентны.

```typescript
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'.
}
```

## Пересечение объектов

В прошлой части мы говорили про операцию пересечения типов (`&`). Для простых типов эта операция почти не имеет смысла, но для типов объектов она очень важна. Предположим, у нас есть два тип объекта.

```typescript
interface A {
    a: number;
}

interface B {
    b: string;
}
```

Рассмотрим два этих типа с точки зрения множеств. Тип `A` является множеством всех объектов, у которых есть поле `a`, значение которого принадлежит множеству `number`. Тип `B` - множество объектов с полем `b`, тип которого `string`. Из-за того, что TypeScript использует утиную типизацию, то типу `A` может подходить объект, который имеет поле `a: number` и еще любое количество других полей с любым типом. Точно также для `B`.

```typescript
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`, и любое количество других полей.

```typescript
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 из типа объекта по названию ключей можно извлекать типы значений этих ключей.

```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 выдаст ошибку.

```typescript
type Wrong = A["e"] //Error: Property 'e' does not exist on type 'A'.
```

Важно, что в `[]` можно передавать не только один ключ, а объединение названий ключей (например, `"a" | "b"`). В результате такого действия мы получим тип, который объединяет типы значений переданных ключей.

```typescript
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`. Он возвращает объединение названий ключей типа объекта, к которому его применили.

```typescript
type Keys = keyof A // "a" | "b" | "c"
```

Так как `keyof` возвращает объединение названий, то его результат можно сразу передать в `[]`, чтобы получить объединение всех типов значений ключей объекта

```typescript
type Q = A[keyof A] //number | string | boolean
```

Возможно сейчас трудно представить, какие задачи решаются при помощи `keyof` и взятии типов значений полей. Эти инструменты являются частями более сложных механизмов TypeScript, хотя могут быть использованы и сами по себе. Например, в TypeScript из коробки предоставляется интерфейс `HTMLElement`. Этот интерфейс является базовым интерфейсом для любого DOM-элемента, который можно получить через [document.createElement](https://github.com/microsoft/TypeScript/blob/master/lib/lib.dom.d.ts#L6737) или каким-то другим способом. Если мы хотим типизировать переменную или поле объекта так, чтобы туда можно было положить значение поля `style` DOM-элемента нам достаточно сделать такое действие.

```typescript
const style: HTMLElement["style"] = {...}
```

Или мы хотим вынести тип поля объекта, не имея типа объекта.

```typescript
const a = {
    a: 10,
    b: "lalaka"
}

type B = (typeof a)["b"] //string
```

## Задания

* [Задание #4](https://www.typescriptlang.org/play?strictNullChecks=false#code/PQKgsAUABDWFggg+EEDwgTCsIIXhBBCIIBhBACIIDhBB5ECXygAEAXAZwFoBTADwAc6BjC+gJ04HtOpA-CDJUgcRAMgaRAogQRBMuBCMBcIPgA0UQEwgUBQkwJc4wKIgUQCwgCQNwggNhB0kwIwg4zIBkQKAkASIPmtL1UQCIg+dNlP26MhQuKjOGPj4AHSQsFCAqCBSpqQagLIgmPiGMnKK2IYubggKUOioUNaAwiBGqJiqWkgC+KaEOrhQACoAyuriRtgyTq7uFZrIjc2AYiCRqkJGctalZUgIlgiCOfg4DlCA7CDVUvYAXLGwABYUFExUh8DAAO4PURQAnixUrJwAlkwUADYAhgA7ADmUT4wOAABMeKwqMBTkDIQAjHg8ADWwE4dF+dH+VDoNEBPAodDhLzeH2+HAAzDQAJxRc4AW1+AGIaNR6Mw2Bw6Nw+DRWDwmUy6IDqJAQMBIJBQCcYIBiEAU2HMmzKOz8YnQRFUIh0thKZVspjWBWsRVGDSahCm+El0ogQsBVAoUH+AEZDlAAN4AXygAF4oABuSBOl1ugBMXu9UAAZp6oICAK5MpF8qD+oOhx08Z2u-7UmPxxPugAMqjj0agFE4yboUAAPlAXV8QZnAyGw3mI-8ACzFhNelNpjP+5uxoct2ufdtZru5-NugCsg-dAH5h6n05xK5HN0nt2POznZeBoLAlSq1VAMgJxLr9ZJSuVxCbBoVivVxjbIvbIDirpIomnaTl67rLlAADUUAQZmObAaB8ZegARACAJov8KHQVAKFMv8GFYfBMoQIBUBItWSFVoORa4eh-yYdh-q+ghkZUYmADa5YALp7jRqH4YRTHERAAF0EBtHsV6tb1iJSLUlJ8YEfiIm3OQnKMCw7BcLwnCQPJUQJp2Ml0KepHieRA4wEGsb-NJdZ0KoSJenGykNix+l9hxKGsCh3Gdq5vz4ghXkoQAjn5AVuSF3lETBKFIpFQYmWZcoXoqyqqpgZR3pIRjNFAADCACCqiFQAQmVhVlQAIo+CAGi+xqmkMFrftatr-hA5INiVJ6QD1RXlf13WvL1hUjYNhU1SN3ZLqwiZ9UGaEEQxWE5uGrqsNWS1QAtOELRtPZbbRFWduWR3zVZZ1BoFwVzRGrCrkVE02SW4EAGwiZte0fV6hWvT6717QpHmLo9ADs-0zUGHHcRtENREwyZUKcAAUtmofRjGZgAlAjSMo+jsaxdh8WJdxAmrTjvr4yRaVxIAGCBSEIAQ7BUhCCKgoRoOg6AVIAciBQJCqgIAI3PYK06g7C0RC3vgAjYIIawKKYAsiNgSCqD1PBxqoaJ0M8utQHa6U1mNbq-J8eI0JINXFVANBQDVABiUAVJlN65cYBX247zsu-VjVGm+LWfpaP62jEEBSg9rqQqB8rAx9FZJ9RdHU1hyhp7R3pJ3EcZWXddBJ762cQL6JGDX72YDRbNXDbXo0sM7gM5tXMMLtXACik3127Tdx8LiY18Ll0RpC1YN+dy5ljmZGQrRNWA3hmcoTmi+dtSZZz0PkJWTVncpXvy4APqJihCbYc2l+RtfuFxtS2HLVf4-x89NW90GkJn+6b-Cx9c+qEr5NgfnfUBl8n4QMLs-B+fZ157z+gHTskJAF-3pueJmLMzD2HZpzb23cHZO27jNUgYsJatEkAAAwAGRUJNlAKhjYqGqA9tebK8t7w+05nQd0-s6B9nLnEPUDVnwh3fGaNqYwOp-hjg6QahCkJ2UPKOfgldm4Nm7o3IGzlpxtmBJmOuLdu6A1jKwA8KIeA4iBIYjRUASFKK9BxEcO5-LqKHrwr0NllHlicljNeYMfp0GrN48Cy4-G4UEmtFCqhWAuTcoE46UA6C0RsmDMiAivE+mUSnMsYMBGgWUVxZc3EWJAA)

* [Тест](https://docs.google.com/forms/d/e/1FAIpQLSc8imy1IR8rPN5z4pMdmlYNJnKks0NMdNnh3OnjFMqvVkf0Jw/viewform?usp=sf_link)

* Пример решения задания #4
