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

Object, Array, Tuple

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

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 ссылочный массивоподобный тип

Ссылочный тип данных 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 в любой его части, а не только в конце.

type Strings = [string, string];
type Numbers = [number, number];

// type Mixed = [string, string, number, number]
type Mixed = [...Strings, ...Numbers];

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

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

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

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

Механизм объявления множественного распространения (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 подобного типа не существует.