Интерфейсы
Одним из основных принципов TypeScript является то, что типизация основана на структуре объектов. Такой способ типизации называют неявной
или «утиной»
— объект относят к тому или иному типу (классу, интерфейсу), если он имеет (реализует) все его свойства и методы. Интерфейсы в TS применяются как раз для того, чтобы описывать нужные вам типы.
Простой пример
Лучший способ узнать, как работают интерфейсы - начасть с простого примера:
let giveFruit = (fruit: { name: string }) => console.log('Give to me ' + fruit.name)
let myFruits = { name: 'Banana', sweetness: 7, bones: false }
giveFruit(myFruits)
Функция giveFruit()
имеет единственный параметр, который требует, чтобы переданный объект имел свойство с именем name
типа string
. Обратите внимание, что наш объект на самом деле имеет больше свойств, чем требуется, но компилятор только проверяет, присутствуют ли хотя бы те, которые необходимы, и соответствуют требуемым типам.
Напишем тот же пример, для проверки свойства name
с типом string
, но при помощи интерфейсов.
interface Fruit {
name: string
sweetness: number
bones: boolean
}
let giveFruit = (fruit: Fruit) => console.log('Give to me ' + fruit.name)
let myFruits = { name: 'Banana', sweetness: 7, bones: false }
giveFruit(myFruits)
Интерфейс Fruit
- это имя, которое мы теперь можем использовать для описания требования в предыдущем примере. Обратите внимание, что нам не нужно было явно указывать, что объект, который мы передаем в функцию giveFruit()
, наследует этот интерфейс, как это может быть в других языках. Здесь важен только образец. Если объект, который мы передаем функции, соответствует перечисленным требованиям, то всё позволено.
Стоит отметить, что проверка типов не требует, чтобы эти свойства имели какой-либо порядок, а только то, что свойства, необходимые для интерфейса, присутствуют и имеют требуемый тип.
Необязательные свойства
Не все свойства интерфейса могут быть обязательными. Некоторые существуют при определенных условиях или могут вообще отсутствовать. Интерфейсы с необязательными свойствами записываются аналогично другим интерфейсам, где каждое необязательное свойство обозначается знаком ?
в конце имени свойства в декларации.
interface Fruit {
name: string
sweetness: number
bones: boolean
color?: number
}
let banana: Fruit = {
name: 'Banana',
sweetness: 7,
bones: false,
color: 0xffe135
}
let apple: Fruit = {
name: 'Apple',
sweetness: 5,
bones: true
}
Необязательные свойства популярны при создании шаблонов, таких как «option bags», в которых вы передаете объект в функцию, у которого заполнены только пара свойств.
Только для чтения
Некоторые свойства могут быть заданы только для чтения, а значение они получат при создании объекта. Этого можно добиться, поместив ключевое слово readonly перед именем свойства.
interface Point {
readonly x: number;
readonly y: number;
}
let a1: Point = { x: 10, y: 40 }
console.log('Точка [' + a1.x + '; ' + a1.y + ']')
Можно создать переменную c типом Point
, присвоив ей литерал объекта. После этого значения свойств x
и y
изменять будет нельзя.
Лишние свойства
В нашем первом примере использования интерфейсов TypeScript позволил передать { name: string; sweetness: number, bones: boolean }
там, где ожидалось всего лишь { name: string }
. Также мы узнали о необязательных свойствах, и о том, как они могут быть полезны при передаче аргументов в функции. Рассмотрим пример.
interface Fruit {
name: string
sweetness?: number
bones?: boolean
color?: number
}
function addFruit(x: Fruit): { name: string; color: number } {
// ...
}
let banana = addFruit({ name: 'banana', colour: 0xffe135 })
// error: 'colour' not expected in type 'Fruit'
Обратите внимание, что аргумент, передаваемый в addFruit()
, записан как colour
вместо color
. В чистом JavaScript подобные вещи не выдают ошибок, но и не работают так, как хотел бы разработчик.
Можно сказать, что данная программа корректна с точки зрения типов, так как типы свойств sweetness
совместимы, color
отсутствует, а наличие дополнительного свойства colour
не имеет никакого значения.
Однако TypeScript делает предположение, что в этом куске кода есть ошибка. Литералы объектов обрабатываются им по-особенному, и проходят проверку на наличие лишних свойств. Эта проверка делается, когда литералы либо присваиваются другим переменным, либо передаются в качестве аргументов. Если в литерале есть какие-либо свойства, которых нет в целевом типе, то это будет считаться ошибкой.
Обойти такую ошибку можно несколькими способами.
Первый способ
Использование приведение типов:
let banana = addFruit({ name: 'banana', colour: 0xFFE135 } as Fruit)
Второй способ
Добавление строкового индекса, его лучше использовать тогда, когда вы уверены, что объект может иметь дополнительные свойства.
interface Fruit {
name: string
color?: number
[propName: string]: any
}
В данном примере интерфейс Fruit
может иметь любое количество свойств. Если это не name
или color
, то тип свойства не имеет значения.
Третий способ
Присвоить объект другой переменной. Из-за присваивания объекта другой переменной он не будет проходить проверку на избыточные свойства, компилятор не выдаст ошибки.
let options = { name: 'banana', colour: 0xffe135 },
banana = addFruit(options)
Стоит иметь ввиду, что для простого кода не стоит обходить данные проверки свойств. Для более сложных литералов объектов, которые содержат в себе методы, параметры состояния и т.д., стоит держать в памяти данные способы обхода проверок, но все же большинство ошибок, связанных с проверкой лишних свойств, как правило, на самом деле являются ошибками. Если у вас возникает такая ошибка, возможно стоит пересмотреть объявление типа.
Типы функций
Помимо описания свойств, интерфейсы также позволяют описывать типы функций.
Для описания типа функции в интерфейсе, в нем нужно определить сигнатуру вызова. Это похоже на объявление функции только со списком параметров и типом возвращаемого значения. Каждый параметр в списке должен иметь имя и тип.
interface SearchFunc {
(source: string, subString: string): boolean
}
Определив такой интерфейс один раз, мы можем его использовать также как и все другие интерфейсы. Пример ниже показывает, как определить переменную с типом функции и присвоить ей значение.
interface SearchFunc {
(source: string, subString: string): boolean
}
let mySearch: SearchFunc
mySearch = function (source: string, subString: string) {
let result = source.search(subString)
if (result == -1) {
return false
} else {
return true
}
}
console.log(mySearch('banana lime apple', 'banana'))
Имена параметров не обязательно должны совпадать, чтобы функция прошла проверку на соответствие типов. Мы, к примеру, могли бы записать предыдущий пример — вот так:
interface SearchFunc {
(source: string, subString: string): boolean
}
let mySearch: SearchFunc
mySearch = (src: string, sub: string): boolean => {
let result = src.search(sub)
if (result == -1) {
return false
} else {
return true
}
}
console.log(mySearch('banana lime apple', 'banana'))
Параметры функций проверяются друг за другом, и типы параметров, находящихся на соответствующих позициях, сравниваются попарно. Если вы не хотите указывать типы для аргументов, то TypeScript сможет вывести типы из контекста, основываясь на том, что функция присваивается переменной, тип которой — SearchFunc. В следующем примере тип возвращаемого значения функции тоже выводится: это делается на основании значений, которые она возвращает false
и true
. Если бы функция возвращала числа или строки, то компилятор во время проверки типов предупредил бы, что тип возвращаемого значения не совпадает с типом, указанным в интерфейсе SearchFunc.
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc
mySearch = (src, sub) => {
let result = src.search(sub)
if (result == -1) {
return false
} else {
return true
}
}
console.log(mySearch('banana lime apple', 'banana'))
Индексируемые типы
Аналогично тому, как мы можем использовать интерфейсы для описания типов функций, мы также можем описывать типы, в которые мы можем «индексировать», например, a[10]
или ageMap["daniel"]
. Индексируемые типы имеют сигнатуру индекса, которая описывает типы, которые мы можем использовать для индексации объекта, вместе с соответствующими типами возврата при индексации.
interface StringArray {
[index: number]: string
}
let myArray: StringArray
myArray = ['Bob', 'Fred']
let myStr: string = myArray[0]
console.log(myArray[0])
Здесь у нас есть интерфейс StringArray
, у которого есть сигнатура индекса. Эта сигнатура говорит о том, что, когда StringArray
индексируется числом, возвращается строка.
Расширение интерфейсов
Интерфейсы могут расширять друг друга. Это позволяет вам копировать элементы одного интерфейса в другой, что дает вам больше гибкости в том, как вы разделяете свои интерфейсы на повторно используемые компоненты.
interface Shape {
color: string
}
interface PenStroke {
penWidth: number
}
// множественное расширение
interface Square extends Shape, PenStroke {
sideLength: number
}
let square = {} as Square
square.color = 'blue'
square.sideLength = 10
square.penWidth = 5.0
Гибридные типы
Как мы упоминали ранее, интерфейсы могут описывать более сложные типы, присутствующие в реальном мире JavaScript. Из-за динамического и гибкого характера JavaScript вы можете случайно встретить объект, который работает как комбинация некоторых типов, описанных выше.
Одним из таких примеров является объект, который действует как функция и объект с дополнительными свойствами:
interface Counter {
(start: number): string
interval: number
reset(): void
}
function getCounter(): Counter {
let counter = function (start: number) {} as Counter
counter.interval = 123
counter.reset = function () {}
return counter
}
let c = getCounter()
c(10)
c.reset()
c.interval = 5.0
Вопросы
Как называется способ типизации, используемый в TypeScript?
- явный
- утиный
- строгий
С помощью какого ключевого слова объявляется интерфейс?
- interface
- class
- function
С помощью какого символа объявляется необязательное свойство?
!
?
-
Для чего используется readonly
?
- Только для чтения
- Только для записи
- Незнаю
Позволяют ли интерфейсы описывать типы функций?
true
false
С помощью какого ключевого слова расширяются интерфейсы?
- yield
- extends
- export
Теперь мы готовы с вами изучать TypeScript, но для того чтобы понять на сколько вы усвоили этот урок пройдите тест в мобильном приложении в нашей школы по этой теме.
Ссылки
Contributors ✨
Thanks goes to these wonderful people (emoji key):
IIo3iTiv | Dmitriy Vasilev 💵 |