TypeScript: Типы

Простое руководство по типу данных «интерфейс» в TypeScript

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

Интерфейс - это форма объекта. Стандартный объект JavaScript - это карта key:value пар. Ключи объектов JavaScript почти во всех случаях являются строками, а их значения - любыми поддерживаемыми значениями JavaScript (примитивными или абстрактными).

Интерфейс сообщает компилятору TypeScript об именах свойств, которые может иметь объект, и соответствующих им типах значений. Следовательно, interface - это тип и абстрактный тип, поскольку он состоит из примитивных типов.

Когда мы определяем объект со свойствами (ключами) и значениями, TypeScript создает неявный интерфейс, просматривая имена свойств и тип данных их значений в объекте. Это происходит из-за вывода типа.

В приведенном выше примере мы создали объект student с полями firstName, lastName, age и getSalary и присвоили некоторые начальные значения. Используя эту информацию, TypeScript создает неявный тип интерфейса для student.

{
    firstName: string;
    lastName: string;
    age: number;
    getSalary: (base: number) => number;
}

Интерфейс похож на объект, но он содержит только информацию о свойствах объекта и их типах. Мы также можем создать тип интерфейса и дать ему имя, чтобы мы могли использовать его для аннотирования значений объекта, но здесь у этого интерфейса нет имени, поскольку он был создан неявно. Вы можете сравнить это с типом функции в предыдущем уроке, который сначала был создан неявно, а затем мы создали тип функции явно, используя псевдоним типа.

Попробуем поработать со свойствами объекта после того, как он был определен.

Как видно из приведенного выше примера, TypeScript запоминает форму объекта, поскольку тип ross является неявным интерфейсом. Если мы попытаемся переопределить значение свойства значением другого типа, отличного от указанного в интерфейсе, или попытаемся добавить новое свойство, которое не указано в интерфейсе, компилятор TypeScript не скомпилирует программу.

Если вы хотите, чтобы объект имел какое-либо свойство, вы можете явно отметить значение any, и компилятор TypeScript не будет определять тип из присвоенного значения объекта. Есть и другие способы добиться именно этого, и мы рассмотрим их в этой статье.

Объявление интерфейса

Хотя неявный интерфейс, который мы видели до сих пор, технически является типом, но он не был определен явно. Как уже говорилось, интерфейс - это не что иное, как форму, которую может принимать объект. Если у вас есть функция, которая принимает аргумент, который должен быть объектом, но определенной формы, то нам нужно аннотировать этот аргумент (параметр) с помощью типа интерфейса.

В приведенном выше примере мы определили функцию getPersonInfo, которая принимает аргумент объекта, который имеет поля firstName, lastName, age и getSalary с указанными типами данных. Обратите внимание, что мы использовали объект, который содержит имена свойств и соответствующие им типы, как тип, использующий аннотацию :<type>. Это пример анонимного интерфейса, поскольку у интерфейса нет имени, он был встроен.

Все это кажется немного сложным. Если объект ross становится более сложным и его нужно использовать в нескольких местах, TypeScript просто кажется вам тем, что вам изначально нравилось, но теперь с ним сложно иметь дело. Чтобы решить эту проблему, мы определяем тип интерфейса с помощью ключевого слова interface.

В приведенном выше примере мы определили интерфейс Person, который описывает форму объекта, но на этот раз у нас есть имя, которое мы можем использовать для ссылки на этот тип. Мы использовали этот тип для аннотирования переменной ross, а также аргумента person функции getPersonIfo. Это проинформирует TypeScript о необходимости проверки этих сущностей на соответствие форме Person.

Зачем нужен интерфейс?

Тип интерфейса может иметь значение для принудительного определенной формы. Обычно в JavaScript мы слепо верим во время выполнения, что объект всегда будет содержать определенное свойство, и это свойство всегда будет иметь значение определенного типа, например {age: 21, ...}.

Когда мы фактически начинаем выполнять операции с этим свойством без предварительной проверки, существует ли это свойство для объекта или соответствует ли его значение ожидаемому, что-то может пойти не так, и это может оставить ваше приложение непригодным для использования впоследствии. Например, {age: '21', ...}, здесь age значение - это string.

Интерфейсы предоставляют безопасный механизм для работы с такими сценариями во время компиляции. Если вы случайно используете свойство несуществующего объекта или значение свойства в недопустимой операции, компилятор TypeScript не скомпилирует вашу программу. Давайте посмотрим на пример.

В приведенном выше примере мы пытаемся использовать свойство name аргумента _student внутри функции printStudent. Поскольку аргумент _student является типом интерфейса Student, компилятор TypeScript выдает ошибку во время компиляции, поскольку этого свойства нет в интерфейсе Student.

Точно так же 100 — _student.firstName не является допустимой операцией, поскольку свойство firstName относится к типу string, и в прошлый раз, когда я проверял, вы не можете вычесть string из number JavaScript (приводит к NaN).

В приведенном выше примере мы использовали традиционный способ записи типа функции для поля getSalary. Однако вы также можете использовать синтаксис функции без тела, что обычно используется в интерфейсах.

interface Student {
    firstName: string;
    lastName: string;
    age: number;
    getSalary(base: number): number;
};

Дополнительные свойства

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

Такие свойства называются необязательными. Интерфейс может содержать необязательные свойства, и мы используем аннотацию ?:Type для их представления, как и необязательные параметры функции.

В приведенном выше примере интерфейс Student имеет свойство age, которое является необязательным. Однако, если свойство age предоставлено, оно должно иметь значение типа number.

В случае объекта ross, который является типом интерфейса Student, мы не предоставили значение для свойства age, которое является допустимым, однако в случае monica мы предоставили свойство age, но его значение равно string, что не является допустимым. юридический. Следовательно, компилятор TypeScript выдает ошибку.

Ошибка может показаться странной, но на самом деле она имеет смысл. Если свойство age не существует для объекта, object.age вернет undefined, который является типом undefined. Если он существует, то значение должно быть типа number.

Следовательно, значение свойства age может иметь тип undefined или number, который в TypeScript представлен с использованием синтаксиса объединения number | undefined.

💡 Мы изучим объединения типов в уроке Система типов.

Однако необязательные свойства создают серьезные проблемы во время выполнения программы. Представим, что мы используем свойство age в арифметической операции, но его значение равно undefined. Это своего рода серьезная проблема.

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

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

💡 Однако для вышеуказанной программы мы установили флаг --strictNullChecks на false, который является флагом компилятора TypeScript. Если мы предоставим эту опцию, вышеуказанная программа компилируется нормально.

Чтобы избежать этой ошибки или предупреждения, нам нужно явно указать компилятору TypeScript, что это свойство является типом number, а не number или undefined. Для этого мы используем утверждение типа (преобразование или приведение типов AKA).

В приведенной выше программе мы использовали (_student.age as number), который преобразует тип _student.age из number | undefined в number. Это способ сказать компилятору TypeScript: «Эй, это число». Но лучший способ справиться с этим - также проверить, является ли _student.age undefined во время выполнения, а затем выполнить арифметическую операцию.

💡 Мы узнаем о утверждениях типов на уроке Система типов.

Тип функции с использованием интерфейса

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

interface InterfaceName {
  (param: Type): Type;
}

Синтаксис объявления интерфейса как типа функции аналогичен самой сигнатуре функции. Как видно из приведенного выше примера, тело интерфейса содержит точную подпись анонимной функции, конечно, без тела. Здесь имена параметров не имеют значения.

В приведенном выше примере мы определили интерфейс IsSumOdd, который определяет тип функции, который принимает два аргумента типа number и возвращает значение boolean. Теперь вы можете использовать этот тип для описания функции, потому что тип интерфейса IsSumOdd эквивалентен типу функции (x: number, y: number) => boolean.

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

В приведенном выше примере мы добавили свойства type и calculate в интерфейс IsSumOdd, который описывает функцию. Используя метод Object.assign, мы объединяем свойства type и calculate со значением функции.

Интерфейсы функционального типа могут быть полезны для описания функций-конструкторов. Функция конструктора похожа на класс, чья работа заключается в создании объектов (экземпляров). До ES5 у нас были только функции-конструкторы, имитирующие class в JavaScript. Поэтому TypeScript компилирует классы в функции-конструкторы, если вы ориентируетесь на ES5 или ниже.

💡 Если вы хотите узнать больше о функции конструктора, прочтите эту статью.

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

function Animal( _name ) {
  this.name = _name;
}
var dog = new Animal( 'Tommy' );
console.log( dog.name ); // Tommy

К счастью, нам не нужно работать с функциями-конструкторами, поскольку TypeScript предоставляет ключевое слово class для создания класса, с которым намного проще работать, чем с функцией-конструктором, поверьте мне. Фактически, class в глубине души - это функция-конструктор в JavaScript. Попробуйте приведенный ниже пример.

class Animal{
  constructor( _name ) {
    this.name = _name;
  }  
}
console.log( typeof Animal ); // "function"

Класс и функция-конструктор - это одно и то же. Единственная разница в том, что class дает нам богатый синтаксис ООП для работы. Следовательно, интерфейс типа функции конструктора представляет класс.

В приведенном выше примере мы определили класс Animal с функцией-конструктором, которая принимает аргумент типа string. Вы можете рассматривать это как функцию-конструктор, которая имеет аналогичную сигнатуру конструктора Animal.

AnimalInterface определяет функцию-конструктор, поскольку она имеет анонимную функцию с префиксом new. Это означает, что класс Animal соответствует типу AnimalInterface. Здесь тип интерфейса AnimalInterface эквивалентен типу функции new (sound: string) => any.

Функция createAnimal принимает ctor аргумент типа AnimalInterface, поэтому мы можем передать класс Animal в качестве значения аргумента. Мы не сможем добавить getSound сигнатуру метода класса Animal в AnimalInterface, и причина этого объясняется в уроке Классы.

Индексируемые типы

Индексируемый объект - это объект, к свойствам которого можно получить доступ с помощью подписи индекса, например obj[ 'property' ]. Это способ доступа по умолчанию к элементу массива, но мы также можем сделать это для объекта.

var a = [ 1, 2, 3 ];
var o = { one: 1, two: 2, three: 3 };
console.log( a[0] ); // 1
console.log( a[ 'one' ] ); // 1 (same as `a.one`)

Иногда ваш объект может иметь произвольное количество свойств без какой-либо определенной формы. В этом случае вы можете просто использовать тип object. Однако этот object тип определяет любое значение, кроме number, string, boolean, symbol, null или undefined, как описано в уроке основные типы.

Если нам нужно строго проверить, является ли значение простым объектом JavaScript, у нас может возникнуть проблема. Это можно решить, используя тип интерфейса с индексной подписью для имени свойства.

interface SimpleObject {
  [key: string]: any;
}

Интерфейс SimpleObject определяет форму объекта с string ключами, значения которых могут быть any типом данных. Здесь имя свойства key используется только для заполнителя, поскольку оно заключено в квадратные скобки.

В приведенном выше примере мы определили объекты ross и monica типа SimpleObject interface. Поскольку эти объекты содержат string ключей и значений типа данных any, это совершенно законно.

Если вас смущает ключ 1 в monica, который является типом number, это допустимо, поскольку объекты или элементы массива в JavaScript могут быть проиндексированы с помощью ключей number или string, как показано ниже.

var o = { 0: 'Zero', '1': 'One' };
var a = [ 'Zero', 'One' ];
console.log( o[ '0' ] ); // Zero
console.log( o[ 1 ] ); // One
console.log( a[ 1 ] ); // One
console.log( a[ '1' ] ); // One

Если нам нужно уточнить тип ключей и их значения, мы, безусловно, можем это сделать. Например, мы можем определить индексируемый тип интерфейса с ключами типа number и значениями типа number, если захотим.

💡 Тип ключа подписи индекса должен быть либо string, либо number.

В приведенном выше примере мы определили LapTimes интерфейс, который может содержать имена свойств типа number и значения типа number. Этот интерфейс может представлять структуру данных, которая может быть проиндексирована с помощью number ключей, следовательно, массив ross и объекты monica и joey являются допустимыми.

Однако объект rachel не соответствует форме LapTimes, поскольку ключ one - это string, и к нему можно получить доступ только с помощью string, например rachel[ 'one' ], и ничего больше. Следовательно, компилятор TypeScript выдаст ошибку, как показано выше.

В индексируемом типе интерфейса могут быть некоторые свойства обязательными, а некоторые - необязательными. Это может быть весьма полезно, когда нам нужно, чтобы объект имел определенную форму, но на самом деле не имеет значения, получаем ли мы лишние и нежелательные свойства в объекте.

В приведенном выше примере мы определили интерфейс LapTimes, который должен содержать свойство name со значением string и необязательное свойство age со значением number. Объект типа LapTimes также может иметь произвольные свойства, ключи которых должны быть number, а значения также должны быть number.

Объект ross является допустимым LapTimes объектом, хотя у него нет свойства age, поскольку оно является необязательным. Однако monica имеет свойство age, но его значение равно string, поэтому оно не соответствует интерфейсу LapTimes.

Объект joey также не соответствует интерфейсу LapTimes, поскольку у него есть свойство gender, которое является типом string. У объекта rachel нет свойства name, которое требуется в LapTimes интерфейсе.

💡 Есть некоторые подводные камни, на которые следует обратить внимание при использовании индексируемых типов. Они упоминаются в этой документации.

Расширение интерфейса

Как и классы, интерфейс может наследовать свойства от других интерфейсов. Однако, в отличие от классов в JavaScript, интерфейс может наследовать от нескольких интерфейсов. Мы используем ключевое слово extends для наследования интерфейса.

Расширяя интерфейс, дочерний интерфейс получает все свойства родительского интерфейса. Это очень полезно, когда несколько интерфейсов имеют общую структуру, и мы хотим избежать дублирования кода, перенеся общие свойства в общий интерфейс, который впоследствии может быть унаследован.

В приведенном выше примере мы создали интерфейс Student, который наследует свойства интерфейсов Person и Player.

Множественные объявления интерфейсов

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

В приведенном выше примере мы несколько раз объявляли Person интерфейс. Это приведет к единому объявлению интерфейса Person путем объединения свойств всех объявлений интерфейса Person.

💡 В уроке Классы мы узнали, что class неявно объявляет интерфейс, и интерфейс может расширять этот интерфейс. Итак, если программа имеет класс Person и интерфейс Person, то последний Person тип (интерфейс) будет иметь объединенные свойства между классом и интерфейсом.

Вложенные интерфейсы

Интерфейс может иметь глубоко вложенные структуры. В приведенном ниже примере поле info интерфейса Student определяет форму объекта со свойствами firstName и lastName.

Точно так же совершенно законно, чтобы поле интерфейса имело тип другого интерфейса. В следующем примере поле info интерфейса Student имеет тип Person интерфейса.