TypeScript. Интерфейсы
Интерфейсы TypeScript позволяют описать свой собственный тип данных, перечислив требуемые свойства и методы, дать этому типу название и использовать его в дальнейшем (реализовать этот интерфейс).
Для определения интерфейса используется ключевое слово interface
.
interface ILabelled {
label: string;
}
function printLabel(labelledObj: ILabelled) {
console.log(labelledObj.label);
}
let myObj = {size: 100500, label: 'Label!!!'};
printLabel(myObj);
Мы имеем интерфейс ILabelled
с одним строковым полем label
. Далее мы указываем, что функция printLabel
в параметры принимает объекты, реализующие интерфейс ILabelled
. Т.е. объекты, которые мы передаем на вход функции, обязаны содержать строковое поле label
.
Необязательные свойства
Бывает, что не все свойства интерфейса должны обязательно присутствовать у объекта, который этот интерфейс реализует. Это можно описать следующим способом:
interface IConfig {
color?: string;
width?: number;
}
Еще таким образом предотвращается возможность использования свойств, не описанных в интерфейсе.
Свойства только на чтение (readonly)
Некоторые свойства объекта должны быть заданы при создании объекта и не могут быть модифицированы в дальнейшем. В таком случае можно воспользоваться конструкцией readonly
:
interface Point {
readonly x: number;
readonly y: number;
}
Можно создать объект Point
, присвоив ему литерал объекта. Но после этого значения x
и y
не могут быть изменены.
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // Ошибка!
Когда юзать const
, а когда readonly
?
Очень просто! Для переменных используем const
, для свойств объекта - readonly
.
Проверка на наличие лишних свойств
Рассмотрим на первом примере:
interface ILabelled {
label: string;
}
//...
// Ошибка: not assignable to type 'ILabelled'...
let myObj: ILabelled = {size: 100500, label: 'Label!!!'};
TypeScript говорит нам, что свойство size
не числится в описании интерфейса ILabelled
и является лишним.
Избежать подобной ошибки можно несколькими способами.
Первый - использовать явное указание типа:
let myObj: ILabelled = {size: 100500, label: 'Label!!!'} as ILabelled;
или
let myObj: ILabelled = <ILabelled> { size: 100500, label: 'Label!!!' };
Второй способ - добавить в описание интерфейса свойство со строковым индексом (потерпите, описание будет чуточку дальше):
interface ILabelled {
label: string;
[propName: string]: any;
}
В этом случае ILabelled
может иметь любое количество свойств, и если они не называются label
, то эти свойства могут быть любого типа.
Ну и последний способ пройти проверку на лишние свойства - присвоить объект другой переменной. Данным способом мы как раз и пользовались в самом первом примере, когда передавали объект myObj
в функцию printLabel
:
interface ILabelled {
label: string;
}
function printLabel(labelledObj: ILabelled) {
console.log(labelledObj.label);
}
let myObj = {size: 100500, label: 'Label!!!'};
printLabel(myObj);
Обычно не нужно прибегать к подобным методам обхода проверки, ибо, как правило, если TypeScript указывает на ошибку, то так оно и есть. Может просто необходимо изменить описание интерфейса?
Тип для функций
Можно создавать интерфейсы для функций. Для этого в интерфейсе в круглых скобках указывается названия и тип принимаемых параметров функции, а после двоеточия - тип возвращаемого значения.
interface IFunction {
(paramA: string, paramB: number): boolean;
}
Использование аналогично обычному интерфейсу:
let myFunc: IFunction;
myFunc = function(paramA: string, paramB: number) {
console.log(`${paramA} = ${paramB}`);
return 'asd';
}
Названия параметров функции могут не совпадать с названиями параметров в интерфейсе (учитывается порядок их следования). Типы параметров функции можно также не указывать:
myFunc = function(a, b) {
console.log(`${a} = ${b}`);
return true;
}
myFunc(10, 10); // Ошибка: первая 10 не строковая!
Есть один момент. Я не понял, как в описании интерфейса указать, что функция не должна ничего возвращать. При указании void
, ошибок тайпскрипт не выдает. Но не выдает ошибок и в случае, когда функция при этом что-то возвращает.
Тип для индексируемых свойств
В интерфейсе можно описать свойства, которые имеют индексы, т.е. формат a[0]
или user['name']
. Делается это схоже с тем, как описываются типы функций, только тип индекса указывается в квадратных скобках:
interface IArray {
[index: number]: string;
}
let myArray: IArray;
myArray = ['a', 'b'];
Тип индекса собственно может быть только числовым или строковым.
Индекс можно сделать readonly
. Тогда выполнить myArray[777]='Я'
не выйдет.
Реализация интерфейса классом
Классы могут реализовывать интерфейсы следующим образом:
interface Flyable {
fly (hight: number);
}
class Ducks implements Flyable {
hight: number;
fly(hight) {
this.hight = hight;
}
}
Для этого используется ключевое слово implements
.
Реализуя какой-то интерфейс, класс как бы заключает соглашение о выполнении определенных требований, перечисленных в описании интерфейса. В данном примере класс "Утки" реализует интерфейс "Способны летать". Если у класса не будет метода fly
, то TypeScript сообщит об ошибке.
Класс может реализовывать несколько интерфейсов (перечисляются через запятую):
class Ducks implements Flyable, Swimmable {
//...
}
Наследование интерфейсов
В TypeScript есть возможность наследовать один интерфейс от другого. Для этого используется ключевое слово extends
(англ. расширять, раздвигать). Это позволяет одному интерфейсу приобретать свойства и методы другого интерфейса, что добавляет некоторой гибкости.
interface IShape {
color: string;
}
interface ICircle extends IShape {
diameter: number;
}
let circle = <ICircle>{};
circle.color = "red";
circle.diameter = 100500;
Можно наследоваться от нескольких интерфейсов, перечислив их через запятую:
interface ICircle extends IShape, IStroke {
//...
}
Наследование от классов
Интерфейс может унаследовать какой-то класс. При этом будут унаследованы все свойства и методы этого класса, включая приватные и защищенные (само собой без реализации). Синтаксис аналогичный:
interface ICircle extends Circle {
/...
}
Но есть заморочка именно с этими приватными и защищенными свойствами класса. Если интерфейс их унаследовал, то реализовать такой интерфейс смогут только сам этот класс, либо его наследники. Это можно использовать для того, чтобы некоторый код работал только с определенными классами, у которых есть определенные методы.
Уффф, объемная тема...