ТипScript для начинающих, часть 3: Интерфейсы
() translation by (you can also view the original English article)
Мы начали эту серию с вводного учебника, в котором вы познакомились с различными функциями TypeScript. Он также научил вас устанавливать TypeScript и предложил некоторые IDE, которые вы можете использовать для написания и компиляции собственного кода.
Во втором учебном пособии мы рассмотрели различные типы данных, доступные в TypeScript, и то, как их использование может помочь вам избежать множества ошибок. Назначение типа данных, такого как string
, для конкретной переменной, сообщает TypeScript, что вы хотите назначить ей только строку. Опираясь на эту информацию, TypeScript может указать вам позже, когда вы пытаетесь выполнить операцию, которая не должна выполняться со строками.
В этом уроке вы узнаете об интерфейсах в TypeScript. С интерфейсами вы можете пойти еще дальше и определить структуру или тип более сложных объектов в вашем коде. Подобно простым типам переменных, эти объекты также должны будут следовать набору правил, созданных вами. Это может помочь вам более уверенно написать код, с меньшей вероятностью ошибки.
Создание нашего первого интерфейса
Предположим, у вас есть объект озера в вашем коде, и вы используете его для хранения информации о некоторых из крупнейших озер по всему миру. Этот объект озера будет иметь свойства, такие как название озера, его площадь, длина, глубина и страны, в которых это озеро существует.
Названия озер будут храниться в виде строки. Длины этих озер будут в километрах, а площади будут в квадратных километрах, но оба этих свойства будут сохранены в виде чисел. Глубины озер будут в метрах, и они могут быть float.
Поскольку все эти озера очень большие, их береговые линии, как правило, не ограничиваются одной страной. Мы будем использовать массив строк для хранения имен всех стран на берегу конкретного озера. Булевы могут использоваться, чтобы указать, является ли озеро озером соленой водой или пресной водой. Следующий фрагмент кода создает интерфейс для нашего объекта на озере.
1 |
interface Lakes { |
2 |
name: string, |
3 |
area: number, |
4 |
length: number, |
5 |
depth: number, |
6 |
isFreshwater: boolean, |
7 |
countries: string[] |
8 |
}
|
Интерфейс Lakes
содержит тип каждого свойства, которое мы собираемся использовать при создании наших объектов озера. Если теперь вы попытаетесь присвоить разные типы значений любому из этих свойств, вы получите сообщение об ошибке. Вот пример, который хранит информацию о нашем первом озере.
1 |
let firstLake: Lakes = { |
2 |
name: 'Caspian Sea', |
3 |
length: 1199, |
4 |
depth: 1025, |
5 |
area: 371000, |
6 |
isFreshwater: false, |
7 |
countries: ['Kazakhstan', 'Russia', 'Turkmenistan', 'Azerbaijan', 'Iran'] |
8 |
}
|
Как вы можете видеть, порядок, в котором вы присваиваете значение этим свойствам, не имеет значения. Однако вы не можете опустить значение. Вам нужно будет присвоить значение каждому свойству, чтобы избежать ошибок при компиляции кода.
Таким образом, TypeScript гарантирует, что вы не пропустили ни одно из необходимых значений по ошибке. Вот пример, когда мы забыли присвоить значение свойства depth
для озера.
1 |
let secondLake: Lakes = { |
2 |
name: 'Superior', |
3 |
length: 616, |
4 |
area: 82100, |
5 |
isFreshwater: true, |
6 |
countries: ['Canada', 'United States'] |
7 |
}
|
Снимок экрана ниже показывает сообщение об ошибке в Visual Studio Code после того, как мы забыли указать depth
. Как вы можете видеть, ошибка ясно указывает на то, что нам не хватает свойства depth
для нашего объекта озера.



Создание свойств интерфейса опциональными
Иногда вам может понадобиться свойство только для некоторых конкретных объектов. Например, предположим, вы хотите добавить свойство, чтобы указать месяцы, в которых озеро замерзает. Если вы добавите свойство непосредственно в интерфейс, как мы это делали до сих пор, вы получите сообщение об ошибке для других озер, которые не замерзают и, следовательно, не имеют свойства frozen
. Аналогично, если вы добавите это свойство к озерам, которые были заморожены, но не в объявлении интерфейса, вы все равно получите ошибку.
В таких случаях вы можете добавить знак вопроса (?
) после имени свойства, чтобы установить его как необязательное в объявлении интерфейса. Таким образом, вы не получите ошибку ни для пропущенных свойств, ни для неизвестных свойств. Следующий пример должен это продемонстрировать.
1 |
interface Lakes { |
2 |
name: string, |
3 |
area: number, |
4 |
length: number, |
5 |
depth: number, |
6 |
isFreshwater: boolean, |
7 |
countries: string[], |
8 |
frozen?: string[] |
9 |
}
|
10 |
|
11 |
let secondLake: Lakes = { |
12 |
name: 'Superior', |
13 |
depth: 406.3, |
14 |
length: 616, |
15 |
area: 82100, |
16 |
isFreshwater: true, |
17 |
countries: ['Canada', 'United States'] |
18 |
}
|
19 |
|
20 |
let thirdLake: Lakes = { |
21 |
name: 'Baikal', |
22 |
depth: 1637, |
23 |
length: 636, |
24 |
area: 31500, |
25 |
isFreshwater: true, |
26 |
countries: ['Russia'], |
27 |
frozen: ['January', 'February', 'March', 'April', 'May'] |
28 |
}
|
Использование индексных сигнатур
Дополнительные свойства полезны, когда многие из ваших объектов будут использовать их. Однако, что, если каждое озеро также имеет свой собственный уникальный набор свойств, таких как хозяйственная деятельность, население различных видов флоры и фауны, процветающих в этом озере, или поселения вокруг озера? Добавление большого количества различных опциональных свойств внутри декларации самого интерфейса - не самый лучший способ.
В качестве решения TypeScript позволяет добавлять дополнительные свойства к определенным объектам с помощью индексных сигнатур. Добавление сигнатуры индекса в объявление интерфейса позволяет указать любое количество свойств для разных объектов, которые вы создаете. Вам необходимо внести следующие изменения в интерфейс.
В этом примере я использовал сигнатуру индекса для добавления информации о разных поселениях вокруг озер. Поскольку у каждого озера будут свои поселения, использование дополнительных объектов не было бы хорошей идеей.
1 |
interface Lakes { |
2 |
name: string, |
3 |
area: number, |
4 |
length: number, |
5 |
depth: number, |
6 |
isFreshwater: boolean, |
7 |
countries: string[], |
8 |
frozen?: string[], |
9 |
[extraProp: string]: any |
10 |
}
|
11 |
|
12 |
let fourthLake: Lakes = { |
13 |
name: 'Tanganyika', |
14 |
depth: 1470, |
15 |
length: 676, |
16 |
area: 32600, |
17 |
isFreshwater: true, |
18 |
countries: ['Burundi', 'Tanzania', 'Zambia', 'Congo'], |
19 |
kigoma:'Tanzania', |
20 |
kalemie: 'Congo', |
21 |
bujumbura: 'Burundi' |
22 |
}
|
В качестве другого примера предположим, что вы создаете игру с разными видами врагов. Все эти враги будут иметь некоторые общие свойства, такие как их размер и здоровье. Эти свойства могут быть включены в декларацию интерфейса напрямую. Если каждая категория этих врагов обладает уникальным набором оружия, это оружие может быть включено с помощью сигнатуры индекса.
Свойства только для чтения
При работе с разными объектами вам может потребоваться работать со свойствами, которые должны быть изменены только при первом создании объекта. Вы можете пометить эти свойства как readonly
в объявлении интерфейса. Это похоже на использование ключевого слова const
, но const
предполагается использовать с переменными, а readonly
- для свойств.
TypeScript также позволяет создавать массивы только для чтения с помощью ReadonlyArray<T>
. Создание массива только для чтения приведет к удалению из него всех методов мутации. Это делается для того, чтобы вы не могли впоследствии изменить значение отдельных элементов. Ниже приведен пример использования свойств и массивов только для чтения в объявлениях интерфейсов.
1 |
interface Enemy { |
2 |
readonly size: number, |
3 |
health: number, |
4 |
range: number, |
5 |
readonly damage: number |
6 |
}
|
7 |
|
8 |
let tank: Enemy = { |
9 |
size: 50, |
10 |
health: 100, |
11 |
range: 60, |
12 |
damage: 12 |
13 |
}
|
14 |
|
15 |
|
16 |
// This is Okay
|
17 |
tank.health = 95; |
18 |
|
19 |
// Error because 'damage' is read-only.
|
20 |
tank.damage = 10; |
Функции и интерфейсы
Вы также можете использовать интерфейсы для описания типа функции. Это требует от вас функцию и сигнатуру вызова со списком параметров и возвращаемым типом. Вам также необходимо указать как имя, так и тип для каждого из параметров. Вот пример:
1 |
interface EnemyHit { |
2 |
(name: Enemy, damageDone: number): number; |
3 |
}
|
4 |
|
5 |
let tankHit: EnemyHit = function(tankName: Enemy, damageDone: number) { |
6 |
tankName.health -= damageDone; |
7 |
return tankName.health; |
8 |
}
|
В приведенном выше коде мы объявили интерфейс функции и использовали его для определения функции, которая вычитает урон, нанесенный танку из его здоровья. Как вы можете видеть, вам не нужно использовать одно и то же имя для параметров в объявлении интерфейса и определение для работы кода.
Заключение
В этом руководстве вы познакомились с интерфейсами и тем, как их использовать, чтобы убедиться, что вы пишете более надежный код. Теперь вы сможете создавать свои собственные интерфейсы с дополнительными свойствами и свойствами только для чтения.
Вы также узнали, как использовать индексные сигнатуры для добавления множества других свойств к объекту, которые не включены в декларацию интерфейса. Это руководство предназначалось для начала работы с интерфейсами в TypeScript, и вы можете прочитать больше на эту тему в официальной документации.
В следующем уроке вы узнаете о классах в TypeScript. Если у вас есть какие-либо вопросы, связанные с интерфейсами, дайте мне знать в комментариях.