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

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

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