Статьи

TypeScript для начинающих, часть 3: интерфейсы

Мы начали эту серию с вводного руководства, которое познакомило вас с различными функциями TypeScript. Он также научил вас устанавливать TypeScript и предложил несколько IDE, которые вы можете использовать для написания и компиляции собственного кода TypeScript.

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

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

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

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

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

1
2
3
4
5
6
7
8
interface Lakes {
    name: string,
    area: number,
    length: number,
    depth: number,
    isFreshwater: boolean,
    countries: string[]
}

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

1
2
3
4
5
6
7
8
let firstLake: Lakes = {
    name: ‘Caspian Sea’,
    length: 1199,
    depth: 1025,
    area: 371000,
    isFreshwater: false,
    countries: [‘Kazakhstan’, ‘Russia’, ‘Turkmenistan’, ‘Azerbaijan’, ‘Iran’]
}

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

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

1
2
3
4
5
6
7
let secondLake: Lakes = {
    name: ‘Superior’,
    length: 616,
    area: 82100,
    isFreshwater: true,
    countries: [‘Canada’, ‘United States’]
}

На приведенном ниже снимке экрана показано сообщение об ошибке в коде Visual Studio после того, как мы забыли указать depth . Как видите, ошибка ясно указывает на то, что нам не хватает свойства depth для нашего объекта озера.

Отсутствующие значения свойств в интерфейсе TypeScript

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
interface Lakes {
    name: string,
    area: number,
    length: number,
    depth: number,
    isFreshwater: boolean,
    countries: string[],
    frozen?: string[]
}
 
let secondLake: Lakes = {
    name: ‘Superior’,
    depth: 406.3,
    length: 616,
    area: 82100,
    isFreshwater: true,
    countries: [‘Canada’, ‘United States’]
}
 
let thirdLake: Lakes = {
    name: ‘Baikal’,
    depth: 1637,
    length: 636,
    area: 31500,
    isFreshwater: true,
    countries: [‘Russia’],
    frozen: [‘January’, ‘February’, ‘March’, ‘April’, ‘May’]
}

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Lakes {
    name: string,
    area: number,
    length: number,
    depth: number,
    isFreshwater: boolean,
    countries: string[],
    frozen?: string[],
    [extraProp: string]: any
}
 
let fourthLake: Lakes = {
    name: ‘Tanganyika’,
    depth: 1470,
    length: 676,
    area: 32600,
    isFreshwater: true,
    countries: [‘Burundi’, ‘Tanzania’, ‘Zambia’, ‘Congo’],
    kigoma:’Tanzania’,
    kalemie: ‘Congo’,
    bujumbura: ‘Burundi’
}

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

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

TypeScript также позволяет создавать массивы только для чтения с помощью ReadonlyArray<T> . Создание массива только для чтения приведет к удалению всех методов мутации из него. Это сделано для того, чтобы вы не могли изменить значение отдельных элементов позже. Вот пример использования свойств и массивов только для чтения в объявлениях интерфейса.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
interface Enemy {
    readonly size: number,
    health: number,
    range: number,
    readonly damage: number
}
 
let tank: Enemy = {
    size: 50,
    health: 100,
    range: 60,
    damage: 12
}
 
 
// This is Okay
tank.health = 95;
 
// Error because ‘damage’ is read-only.
tank.damage = 10;

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

1
2
3
4
5
6
7
8
interface EnemyHit {
    (name: Enemy, damageDone: number): number;
}
 
let tankHit: EnemyHit = function(tankName: Enemy, damageDone: number) {
    tankName.health -= damageDone;
    return tankName.health;
}

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

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

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

В следующем уроке вы узнаете о классах в TypeScript. Если у вас есть какие-либо вопросы, связанные с интерфейсами, дайте мне знать в комментариях.