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
интерфейса.