# Объекты

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

Ранее мы рассматривали примитивные типы и массивы. Сейчас мы посмотрим на то, как 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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://race-timo.gitbook.io/typescript/objects.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
