Интерфейс определяет свойства и методы, которые объект должен реализовать. Другими словами, интерфейс - это определение кастомного типа данных, но без реализации. В данном случае интерфейсы в TS похожи на интерфейсы в языках Java и C#. Интерфейсы определяются с помощью ключевого слова interface. Для начала определим простенький интерфейс:
interface IUser { id: number; name: string; }
Интерфейс в фигурных скобках определяет два свойства: id, которое имеет тип number, и name, которая представляет строку. Теперь используем его в программе:
let employee: IUser = { id: 1, name: "Tom" } console.log("id: ", employee.id); console.log("name: ", employee.name);
По сути employee
- обычный объект за тем исключением, что он имеет тип IUser
. Если правильнее говорить,
то employee реализует интерфейс IUser. Причем эта реализация накладывает на employee некоторые ограничения. Так, employee должен
реализовать все свойства и методы интерфейса IUser, поэтому при определении employee данный объект обязательно должен включать в себя
свойства id и name.
Параметры методов и функций также могут представлять интерфейсы:
interface IUser { id: number; name: string; } let employee: IUser = { id: 1, name: "Alice" } function printUser(user: IUser): void { console.log("id: ", user.id); console.log("name: ", user.name) } printUser(employee);
В этом случае аргумент, который передается в функцию, должен представлять объект или класс, который реализует соответствующий интерфейс.
И также можно возвращать объекты интерфейса:
interface IUser { id: number; name: string; } function buildUser(userId: number, userName: string): IUser { return { id: userId, name: userName }; } let newUser = buildUser(2, "Bill"); console.log("id: ", newUser.id); console.log("name: ", newUser.name);
При определении интерфейса мы можем задать некоторые свойства как необязательные с помощью знака вопроса. Подобные свойства реализовать необязательно:
interface IUser { id: number; name: string; age?: number; } let employee: IUser = { id: 1, name: "Alice", age: 23 } let manager: IUser = { id: 2, name: "Tom" }
Свойство age
помечено как необязательное, поэтому его можно не определять в объектах.
Также интерфейс может содержать свойства только для чтения, значение которых нельзя изменять. Такие свойства определяются с помощью ключевого слова readonly:
interface Point { readonly x: number; readonly y: number; } let p: Point = { x: 10, y: 20 }; console.log(p); // p.x = 5; // Ошибка - свойство доступно только для чтения
Кроме свойств интерфейсы могут определять функции:
interface IUser { id: number; name: string; sayWords(words: string): void; } let employee: IUser = { id: 1, name: "Alice", sayWords: function(words: string): void{ console.log(`${this.name} говорит "${words}"`); } } employee.sayWords("Привет, как дела?");
Опять же объект, который реализует интерфейс, также обязан реализовать определенную в интерфейсе функцию с тем же набором параметров и
тем типом выходного результата. В данном случае функция sayWords()
в качестве параметра принимает строку и ничего возвращает,
выводя на консоль некоторое сообщение.
Интерфейсы могут быть реализованы не только объектами, но и классами. Для этого используется ключевое слово implements:
interface IUser { id: number; name: string; getFullName(surname: string): string; } class User implements IUser{ id: number; name: string; age: number; constructor(userId: number, userName: string, userAge: number) { this.id = userId; this.name = userName; this.age = userAge; } getFullName(surname: string): string { return this.name + " " + surname; } } let tom = new User(1, "Tom", 23); console.log(tom.getFullName("Simpson"));
Класс User реализует интерфейс IUser. В этом случае класс User обязан определить все те же свойства и функции, которые есть в IUser.
При этом объект tom
является как объектом User, так и объектом IUser:
let tom :IUser = new User(1, "Tom", 23); //или let tom :User = new User(1, "Tom", 23);
TypeScript позволяет добавлять в интерфейс новые поля и методы, просто объявив интерфейс с тем же именем и определив в нем необходимые поля и методы. Например:
interface IUser { id: number; name: string; } interface IUser{ age: number; } let employee: IUser = { id: 1, name: "Alice", age: 31 } function printUser(user: IUser): void { console.log(`id: ${user.id} name: ${user.name} age: ${user.age}`); } printUser(employee);
В данном случае первое определение интерфейса IUser
содержит поля id
и name
. Второе определение интерфейса
содержит объявление поля age
. В итоге объект или класс, который реализует этот интерфейс, должен определить все три поля - id, name и age.
Интерфейсы, как и классы, могут наследоваться:
interface IMovable { speed: number; move(): void; } interface ICar extends IMovable { fill(): void; } class Car implements ICar { speed: number; move(): void { console.log("Машина едет со скоростью " + this.speed + " км/ч"); } fill(): void { console.log("Заправляем машину топливом"); } } let auto = new Car(); auto.speed = 60; auto.fill(); auto.move();
После наследования интерфейс ICar будет также иметь все те свойства и функции, которые определены в IMovable. И тогда, класс Car, реализующий интерфейс ICar, должен будет реализовать также и свойства и методы интерфейса IMovable.
Интерфейсы функций содержат определение типа функции. Затем они должны быть реализованы объектом, который представляет функцию данного типа:
interface FullNameBuilder { (name: string, surname: string): string; } let simpleBuilder: FullNameBuilder = function (name:string, surname: string): string { return "Mr. " + name + " " + surname; } let fullName = simpleBuilder("Bob", "Simpson"); console.log(fullName); // Mr. Bob Simpson
Здесь определен интерфейс FullNameBuilder, который лишь содержит сигнатуру функции. Далее определяется переменная simpleBuilder, которая имеет тип FullNameBuilder и поэтому должна представлять функцию с данной сигнатурой.
Интерфейсы массивов описывают объекты, к которым можно обращаться по индексу, как, например, к массивам
interface StringArray { [index: number]: string; } let phones: StringArray; phones = ["iPhone 7", "HTC 10", "HP Elite x3"]; let myPhone: string = phones[0]; console.log(myPhone);
Здесь определен интерфейс StringArray, который содержит сигнатуру массива. Эта сигнатура указывает, что объект, который реализует StringArray,
может индексироваться с помощью чисел (объекта типа number). И, кроме того, данный объект должен хранить объекты типа string
, то есть строки.
Выше индекс представлял тип number. Но мы можем использовать для индексации и тип string:
interface Dictionary { [index: string]: string; } var colors: Dictionary = {}; colors["red"] = "#ff0000"; colors["green"] = "#00ff00"; colors["blue"] = "#0000ff"; console.log(colors["red"]);
Интерфейсы могут сочетать различные стили, могут применяться сразу как к определению объекта, так и к определению функции:
interface PersonInfo { (name: string, surname: string):void; fullName: string; password: string; authenticate(): void; } function personBuilder(): PersonInfo { let person = <PersonInfo>function (name: string, surname: string): void{ person.fullName = name + " " + surname; }; person.authenticate = function () { console.log(person.fullName + " входит в систему с паролем " + person.password); }; return person; } let tom = personBuilder(); tom("Tom", "Simpson"); tom.password = "qwerty"; tom.authenticate();
Тип функции, определяемый в таком гибридном интерфейсе, как правило, выступает в роли конструктора объекта. В данном случае такой конструктор имеет
тип (name: string, surname: string):void;
.
А функция, которая представляет данный интерфейс (в данном случае - функция personBuilder
), реализует эту функцию конструктора, и также может использовать другие
свойства и методы, которые были определены в интерфейсе.