Объекты

Тип объекта

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

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

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

Такой подход к проверке типов называется структурной системой типовarrow-up-right или "утиной типизацией" (duck typingarrow-up-right).

Type alias

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

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

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

typeof

Иногда возникает потребность сохранить тип какой-то уже инициализированной переменной и использовать этот тип в другом месте. Для этого в TypeScript есть оператор typeof.

Очень важно не путать этот оператор с одноименным оператором из JavaScript. TypeScript поддерживает оба оператора. Это возможно потому, что синтаксически они используются в разных местах: один в местах работы с типами, другой - со значениями.

Интерфейс

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

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

Различия между type alias и interface

  • В type alias можно положить не только объект, а любой тип, включая interface или другой type alias.

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

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

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

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

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

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

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

Это далеко не все отличительные особенности интерфейсов и type alias. Далее будут выявляться все больше отличительных особенностей этих конструкций. Однако, уже сейчас можно сделать несколько выводов:

  • Интерфейсы позволяют типизировать только объекты. Поэтому поддерживают некоторые специфические особенности типизации объектов, такие как наследование и расширение.

  • Type alias предназначены для хранения абсолютно любого типа. Из-за этого они не поддерживают специфические особенности типизации объектов, также как и специфические особенности типизации других типов (строк, булевых значений и т.д.). Но так как тип объекта - это тоже тип, а type alias - хранилище для любого типа, отсюда и появляется это двоякая возможность задания типа объекта.

Readonly и опциональные поля

В TypeScript при описании типа объекта можно указать, что поле является не изменяемыми. Делается это при помощи модификатора readonly.

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

Важно отметить что модификатор readonly работает только во время проверки типов. После компиляции readonly (как и все типы) не попадают в результирующие файлы. Поэтому в runtime эти поля можно изменять как угодно.

Часто возникает потребность описать тип объекта, у которого некоторые поля являются опциональными. Для этого в TypeScript можно воспользоваться знаком ?.

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

Индексаторы

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

Индексатор говорит о том, что у объекта нет конкретных названий полей. Известен лишь тип полей и тип значения поля. Тип полей может быть или string, или number. Нельзя использовать литеральные типы или их объединения (по крайней мере в interface-ах и в таком виде). Вместо это нужно объявлять тип с обычными полями.

В одном типе могут быть сразу два индексатора с разным типом полей, но тип значений числового индекса, должен быть подтипом строчного индекса. Это ограничение введено из-за особенностей JavaScript. Когда вы используется числовой индекс [10], JavaScript конвертирует значение индекса в строку. Получается, что с точки зрения JavaScript вызов индекса [10] и ["10"] - эквивалентны.

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

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

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

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

Взятие тип значения ключа. keyof

В TypeScript из типа объекта по названию ключей можно извлекать типы значений этих ключей.

Применяя к типу объекта [] с названием ключа, мы получаем тип значения, который соответствует этому ключу. Если попытаться взять тип значения ключа, которого в объекте нет - TypeScript выдаст ошибку.

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

В TypeScript есть специальный оператор keyof. Он возвращает объединение названий ключей типа объекта, к которому его применили.

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

Возможно сейчас трудно представить, какие задачи решаются при помощи keyof и взятии типов значений полей. Эти инструменты являются частями более сложных механизмов TypeScript, хотя могут быть использованы и сами по себе. Например, в TypeScript из коробки предоставляется интерфейс HTMLElement. Этот интерфейс является базовым интерфейсом для любого DOM-элемента, который можно получить через document.createElementarrow-up-right или каким-то другим способом. Если мы хотим типизировать переменную или поле объекта так, чтобы туда можно было положить значение поля style DOM-элемента нам достаточно сделать такое действие.

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

Задания

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

Last updated