Перейти к содержанию

Object, Array, Tuple

Пришло время рассмотреть такие типы данных как Object и Array, с которыми разработчики JavaScript уже хорошо знакомы. А также неизвестный им тип данных Tuple, который, как мы скоро убедимся, не представляет собой ничего сложного.

Object (object) — ссылочный объектный тип

Ссылочный тип данных Object является базовым для всех ссылочных типов в TypeScript.

Помимо того, что в TypeScript существует объектный тип Object, представляющий одноименный конструктор из JavaScript, также существует тип object, представляющий любое объектное значение. Поведение типа указанного с помощью ключевого слова object и интерфейса Object различаются.

Переменные, которым указан тип с помощью ключевого слова object, не могут хранить значения примитивных типов, чьи идентификаторы (имена) начинаются со строчной буквы (number, string и т.д.). В отличие от них тип интерфейс Object совместим с любым типом данных.

let o: object;
let O: Object;

o = 5; // Error
O = 5; // Ok

o = ''; // Error
O = ''; // Ok

o = true; // Error
O = true; // Ok

o = null; // Error, strictNullChecks = true
O = null; // Error, strictNullChecks = true

o = undefined; // Error, strictNullChecks = true
O = undefined; // Error, strictNullChecks = true

По факту, тип, указанный как object, соответствует чистому объекту, то есть не имеющему никаких признаков (даже унаследованных от типа Object). В то время как значение, ограниченное типом Object, будет включать все его признаки (методы hasOwnProperty() и т.п.). При попытке обратиться к членам объекта, не задекларированным в интерфейсе Object, возникнет ошибка. Напомним, что в случаях, когда тип нужно сократить до базового, сохранив при этом возможность обращения к специфичным (определенным пользователем) членам объекта, нужно использовать тип any.

class SeaLion {
  rotate(): void {}

  voice(): void {}
}

let seaLionAsObject: object = new SeaLion(); // Ok
seaLionAsObject.voice(); // Error

let seaLionAsAny: any = new SeaLion(); // Ok
seaLionAsAny.voice(); // Ok

Тип интерфейса Object идентичен по своей работе одноименному типу из JavaScript. Несмотря на то, что тип указанный с помощью ключевого слова object имеет схожее название, его поведение отличается от типа интерфейса.

Array (type[]) ссылочный массивоподобный тип

Ссылочный тип данных Array является типизированным спископодобным объектом, содержащим логику для работы с элементами.

Тип данных Array указывается с помощью литерала массива, перед которым указывается тип данных type[].

Если при объявлении массива указать тип string[], то он сможет хранить только элементы принадлежащие или совместимые с типом string (например null, undefined, literal type string).

var animalAll: string[] = ['Elephant', 'Rhino', 'Gorilla'];

animalAll.push(5); // Error
animalAll.push(true); // Error
animalAll.push(null); // Ok
animalAll.push(undefined); // Ok

В случае неявного указания типа вывод типов самостоятельно укажет тип как string[].

var animalAll = ['Elephant', 'Rhino', 'Gorilla']; // animalAll : string[]

Если требуется, чтобы массив хранил смешанные типы данных, то один из способов это сделать — указать тип объединение (Union). Нужно обратить внимание на то, как трактуется тип данных Union при указании его массиву. Может показаться, что, указав в качестве типа тип объединение Union, массив (Elephant | Rhino | Gorilla)[] может состоять только из какого-то одного перечисленного типа Elephant, Rhino или Gorilla. Но это не совсем так. Правильная трактовка гласит, что каждый элемент массива может принадлежать к типу Elephant или Rhino, или Gorilla. Другими словами, типом, к которому принадлежит массив, ограничивается не весь массив целиком, а каждый отдельно взятый его элемент.

class Elephant {}
class Rhino {}
class Gorilla {}

var animalAll: (Elephant | Rhino | Gorilla)[] = [
  new Elephant(),
  new Rhino(),
  new Gorilla(),
];

Если для смешанного массива не указать тип явно, то вывод типов самостоятельно укажет все типы, которые хранятся в массиве. Более подробно эта тема будет рассмотрена в главе “Типизация - Вывод типов”.

В случае, если при создании экземпляра массива типы его элементов неизвестны, то следует указать в качестве типа тип any.

let dataAll: any[] = [];

dataAll.push(5); // Ok -> number
dataAll.push('5'); // Ok -> string
dataAll.push(true); // Ok -> boolean

Нужно стараться как можно реже использовать массивы со смешанными типами, а к массивам с типом any нужно прибегать только в самых крайних случаях. Кроме того, как было рассказано в главе “Экскурс в типизацию - Совместимость типов на основе вариантности”, нужно крайне осторожно относиться к массивам, у которых входные типы являются ковариантными.

В случаях, требующих создания экземпляра массива с помощью оператора new, необходимо прибегать к типу глобального обобщённого интерфейса Array<T>. Обобщения будут рассмотрены чуть позднее, а пока нужно запомнить следующее. При попытке создать экземпляр массива путем вызова конструктора, операция завершится успехом в тех случаях, когда создаваемый массив будет инициализирован пустым либо с элементами одного типа данных. В случаях смешанного массива его тип необходимо конкретизировать явно с помощью параметра типа заключенного в угловые скобки. Если сейчас это не понятно, не переживайте, в будущем это будет рассмотрено очень подробно.

let animalData: string[] = new Array(); //Ok
let elephantData: string[] = new Array('Dambo'); // Ok
let lionData: (string | number)[];

lionData = new Array('Simba', 1); // Error
lionData = new Array('Simba'); // Ok
lionData = new Array(1); // Ok
let deerData: (string | number)[] = new Array<
  string | number
>('Bambi', 1); // Ok

В TypeScript поведение типа Array<T> идентично поведению одноимённого типа из JavaScript.

Tuple ([T0, T1, …, Tn]) тип кортеж

Тип Tuple (кортеж) описывает строгую последовательность множества типов, каждый из которых ограничивает элемент массива с аналогичным индексом. Простыми словами кортеж задает уникальный тип для каждого элемента массива. Перечисляемые типы обрамляются в квадратные скобки, а их индексация, так же как у массива начинается с нуля - [T1, T2, T3]. Типы элементов массива, выступающего в качестве значения, должны быть совместимы с типами обусловленных кортежем под аналогичными индексами.

Другими словами, если кортеж составляет последовательность типов string и number, то в качестве значения должен выступать массив, первый элемент которого совместим с типом string, а второй с number. В иных ситуациях неизбежно возникнет ошибка.

let v0: [string, number] = ['Dambo', 1]; // Ok
let v1: [string, number] = [null, undefined]; // Error -> null не string, а undefined не number
let v3: [string, number] = [1, 'Simba']; // Error -> порядок обязателен
let v4: [string, number] = [, ,]; // Error -> пустые элементы массива приравниваются к undefined

Длина массива-значения должна соответствовать количеству типов, указанных в Tuple.

let elephantData: [string, number] = ['Dambo', 1]; // Ok
let liontData: [string, number] = ['Simba', 1, 1]; // Error, лишний элемент
let fawnData: [string, number] = ['Bambi']; // Error, не достает одного элемента
let giraffeData: [string, number] = []; // Error, не достает всех элементов

Но это правило не мешает добавить новые элементы после того, как массив был присвоен ссылке (ассоциирован со ссылкой). Но элементы, чьи индексы выходят за пределы установленные кортежем, обязаны иметь тип, совместимый с одним из перечисленных в этом кортеже.

let elephantData: [string, number] = ['Dambo', 1];
elephantData.push(1941); // Ok
elephantData.push('Disney'); // Ok
elephantData.push(true); // Error, тип boolean, в то время, как допустимы только типы совместимые с типами string и number

elephantData[10] = ''; // Ok
elephantData[11] = 0; // Ok

elephantData[0] = ''; // Ok, значение совместимо с типом заданном в кортеже
elephantData[0] = 0; // Error, значение не совместимо с типом заданном в кортеже

Массив, который связан с типом кортежем, ничем не отличается от обычного, за исключением способа определения типа его элементов. При попытке присвоить элемент под индексом 0 переменной с типом string, а элемент под индексом 1 переменной с типом number, операции присваивания завершатся успехом. Но, несмотря на то, что элемент под индексом 2 хранит значение, принадлежащее к типу string, оно не будет совместимо со string. Дело в том, что элементы, чьи индексы выходят за пределы установленные кортежем, принадлежат к типу объединению (Union). Это означает, что элемент под индексом 2 принадлежит к типу string | number, а это не то же самое что тип string.

let elephantData: [string, number] = ['Dambo', 1]; // Ok

elephantData[2] = 'nuts';

let elephantName: string = elephantData[0]; // Ok, тип string
let elephantAge: number = elephantData[1]; // Ok, тип number
let elephantDiet: string = elephantData[2]; // Error, тип string | number

Есть два варианта решения этой проблемы. Первый вариант, изменить тип переменной со string на тип объединение string | number, что ненадолго избавит от проблемы совместимости типов. Второй, более подходящий вариант, прибегнуть к приведению типов, который детально будет рассмотрен позднее.

В случае, если описание кортежа может навредить семантике кода, его можно поместить в описание псевдонима типа (type).

type Tuple = [number, string, boolean, number, string];

let v1: [number, string, boolean, number, string]; // плохо
let v2: Tuple; // хорошо

Кроме того, тип кортеж можно указывать в аннотации остаточных параметров (...rest).

function f(...rest: [number, string, boolean]): void {}

let tuple: [number, string, boolean] = [5, '', true];
let array = [5, '', true];

f(5); // Error
f(5, ''); // Error
f(5, '', true); // Ok
f(...tuple); // Ok
f(tuple[0], tuple[1], tuple[2]); // Ok
f(...array); // Error
f(array[0], array[1], array[2]); // Error, все элементы массива принадлежат к типу string | number | boolean, в то время как первый элемент кортежа принадлежит к типу number

Помимо этого, типы, указанные в кортеже, могут быть помечены как необязательные с помощью необязательного модификатора ?.

function f(...rest: [number, string?, boolean?]): void {}

f(); // Error
f(5); // Ok
f(5, ''); // Ok
f(5, '', true); // Ok

У кортежа, который включает типы помеченные как не обязательные, свойство длины принадлежит к типу объединения (Union), состоящего из литеральных числовых типов.

function f(
  ...rest: [number, string?, boolean?]
): [number, string?, boolean?] {
  return rest;
}

let l = f(5).length; // let l: 1 | 2 | 3

Кроме того, для кортежа применим механизм распространения (spread), который может быть указан в любой части определения типа. Но существуют два исключения. Во первых, определение типа кортежа может включать только одно распространение.

/**
 * [0] A rest element cannot follow another rest element.ts(1265)
 *
 */
let v0: [...boolean[], ...string[]]; // Error [0]
let v1: [...boolean[], boolean, ...string[]]; // Error [0]

let v2: [...boolean[], number]; // Ok
let v3: [number, ...boolean[]]; // Ok
let v4: [number, ...boolean[], number]; // Ok

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

/**
 * [0] An optional element cannot follow a rest element.ts(1266)
 *
 */
let v5: [...boolean[], boolean?]; // Error [1]

let v6: [boolean?, ...boolean[]]; // Ok

В результате распространения, получается тип с логически предсказуемой последовательностью типов, определяющих кортеж.

type Strings = [string, string];
type BooleanArray = boolean[];

// type Unbounded0 = [string, string, ...boolean[], symbol]
type Unbounded0 = [...Strings, ...BooleanArray, symbol];

// type Unbounded1 = [string, string, ...boolean[], symbol, string, string]
type Unbounded1 = [
  ...Strings,
  ...BooleanArray,
  symbol,
  ...Strings
];

Стоит заметить, что поскольку механизм распространения участвует в рекурсивном процессе формирования типа, способного значительно замедлять компилятор, установленно ограничение в размере 10000 итераций.

Механизм объявления множественного распространения (spread) значительно упрощает аннотирование сигнатуры функции при реализации непростых сценариев, один из которых будет рассмотрен далее в главе (Массивоподобные readonly типы)[].

Еще несколько неочевидных моментов в логике кортежа связанны с выводом типов и будут рассмотрены в главе “Типизация - Вывод типов” (см реализацию функции concat).

Помимо этого семантику типов кортежей можно повышать за счет добавления им меток.

// пример безликого кортежа

const f = (p: [string, number]) => {};

/**
 * автодополнение -> f(p: [string, number]): void
 *
 * Совершенно не понятно чем конкретно являются
 * элементы представляемые типами string и number
 */
f0();
// пример кортежа с помеченными элементами

const f = (p: [a: string, b: number]) => {};

/**
 * автодополнение -> f(p: [a: string, b: number]): void
 *
 * Теперь мы знаем что функция ожидает не просто
 * строку и число, а аргумент "a" и аргумент "b",
 * которые в реальном проекте будут иметь более
 * осмысленное смысловое значение, например "name" и "age".
 */
f1();

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

const f = (p: [a: string, b: number]) => {
  let [c, d] = p;
};

Единственное правило, касающееся данного механизма, заключается в том, что кортеж, содержащий метки, не может содержать элементы описанные только типами.

type T = [a: number, b: string, boolean]; // Error -> Tuple members must all have names or all not have names.ts(5084)

Напоследок стоит обратить внимание на тот факт, что тип переменной при присвоении ей инициализированного массива без явного указания типа, будет выведен как массив. Другими словами, вывод типа неспособен вывести тип кортеж.

let elephantData = ['Dambo', 1]; // type Array (string | number)[]

Тип Tuple является уникальным для TypeScript, в JavaScript подобного типа не существует.