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 {
    /...
}

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

Уффф, объемная тема...

Павел Прудников

Постигающий дзен фулстэк вэб буддизма

Минск, Беларусь

Подписаться на Блог MEAN stack разработчика

Получайте свежие записи прямо на ваш почтовый ящик.

Или подпишитесь на RSS с Feedly!

Комментарии

comments powered by Disqus