DEV Community

Arif Balaev
Arif Balaev

Posted on

[TypeScript] Интерфейсы

Вольный перевод документации Typescript. Interfaces

Одним из основных принципов TypeScript является то, что проверка типов фокусируется на образец, который имеет значения. Это иногда называют «утиной типизацией» или «структурным подтипом». В TypeScript интерфейсы выполняют роль именования этих типов и являются мощным способом определения контрактов в вашем коде, а также контрактов с кодом вне вашего проекта.

Наш первый интерфейс

Самый простой способ увидеть, как работают интерфейсы - начать с простого примера:

function printLabel(labeledObj: { label: string }) {
  console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
Enter fullscreen mode Exit fullscreen mode

Проверка типов проверяет вызов printLabel. Функция printLabel имеет единственный параметр, который требует, чтобы переданный объект имел свойство с именем label типа string. Обратите внимание, что наш объект на самом деле имеет больше свойств, чем требуется, но компилятор только проверяет, присутствуют ли хотя бы те, которые необходимы, и соответствуют требуемым типам. В некоторых случаях TypeScript не такой снисходительный, о чем мы немного позже поговорим.

Мы можем написать тот же пример, на этот раз используя интерфейс, чтобы описать требование наличия свойства label, которое является типом string:

interface LabeledValue {
  label: string;
}

function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
Enter fullscreen mode Exit fullscreen mode

Интерфейс LabeledValue - это имя, которое мы теперь можем использовать для описания требования в предыдущем примере. Он по-прежнему представляет наличие единственного свойства с именем label типа string. Обратите внимание, что нам не нужно было явно указывать, что объект, который мы передаем в printLabel, наследует этот интерфейс, как это может быть в других языках. Здесь важен только образец. Если объект, который мы передаем функции, соответствует перечисленным требованиям, то всё позволено.

Стоит отметить, что проверка типов не требует, чтобы эти свойства имели какой-либо порядок, а только то, что свойства, необходимые для интерфейса, присутствуют и имеют требуемый тип.

Необязательные свойства

Не все свойства интерфейса могут быть обязательными. Некоторые существуют при определенных условиях или могут вообще отсутствовать. Необязательные свойства популярны при создании шаблонов, таких как «option bags», в которых вы передаете объект в функцию, у которого заполнены только пара свойств.

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = { color: "white", area: 100 };
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({ color: "black" });
Enter fullscreen mode Exit fullscreen mode

Интерфейсы с необязательными свойствами записываются аналогично другим интерфейсам, где каждое необязательное свойство обозначается знаком ? в конце имени свойства в декларации.

Свойства только для чтения

Некоторые свойства могут быть изменены только при первом создании объекта. Вы можете указать это, поместив readonly перед именем свойства:

interface Point {
  readonly x: number;
  readonly y: number;
}
Enter fullscreen mode Exit fullscreen mode

TypeScript также работает с типом ReadonlyArray<T>, аналогичным типу Array<T>, только с удаленными методами мутирования, поэтому вы можете быть уверены, что не изменили свои массивы после создания:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;

ro[0] = 12; // ошибка!
//Index signature in type 'readonly number[]' only permits reading.

ro.push(5); // ошибка!
// Property 'push' does not exist on type 'readonly number[]'.

ro.length = 100; // ошибка!
// Cannot assign to 'length' because it is a read-only property.

a = ro; // ошибка!
// The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
Enter fullscreen mode Exit fullscreen mode

Типы функций

Интерфейсы способны описывать широкий спектр образцов, которые могут принимать JavaScript объекты. В дополнение к описанию объекта со свойствами, интерфейсы также способны описывать типы функций.

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

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;

// названия параметров не обязательно должны совпадать
mySearch = function (src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
};
Enter fullscreen mode Exit fullscreen mode

Индексируемые типы

Аналогично тому, как мы можем использовать интерфейсы для описания типов функций, мы также можем описывать типы, в которые мы можем «индексировать», например, a[10] или ageMap["daniel"]. Индексируемые типы имеют сигнатуру индекса, которая описывает типы, которые мы можем использовать для индексации объекта, вместе с соответствующими типами возврата при индексации. Давайте посмотрим на пример:

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];
Enter fullscreen mode Exit fullscreen mode

Выше у нас есть интерфейс индексируемого String Array. Индексная сигнатура гласит, что когда String Array проиндексирован числом, он вернет строку.

Типы классов

В TypeScript также возможно одно из наиболее распространенных применений интерфейсов в таких языках, как C# и Java, - явное принудительное принудительное соблюдение классом определенного контракта.

interface ClockInterface {
  currentTime: Date; // переменные
  setTime(d: Date): void; // методы
}

class Clock implements ClockInterface {
  currentTime: Date = new Date();
  setTime(d: Date) {
    this.currentTime = d;
  }  
  constructor(h: number, m: number) {}
}
Enter fullscreen mode Exit fullscreen mode

Разница между статической и экземплярной сторонами классов

При работе с классами и интерфейсами полезно помнить, что класс имеет два типа: статические и экземпларные. Вы можете заметить, что если вы создаете интерфейс с сигнатурой конструктора и пытаетесь создать класс, реализующий этот интерфейс, вы получаете ошибку:

interface ClockConstructor {
  new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
// Class 'Clock' incorrectly implements interface 'ClockConstructor'. Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.

  currentTime: Date;
  constructor(h: number, m: number) {}
}
Enter fullscreen mode Exit fullscreen mode

Это потому, что когда класс реализует интерфейс, проверяется только сторона экземпляра класса. Поскольку конструктор находится в статической стороне, он не включен в эту проверку.

Вместо этого вам нужно будет работать непосредственно со статической стороной класса. В следующем примере мы определяем два интерфейса: ClockConstructor для конструктора и ClockInterface для методов экземпляра. Затем для удобства мы определяем функцию конструктора createClock, которая создает экземпляры типа, который передается ей:

interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
  tick(): void;
}

function createClock(
  ctor: ClockConstructor,
  hour: number,
  minute: number
): ClockInterface {
  return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("beep beep");
  }
}

class AnalogClock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("tick tock");
  }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
Enter fullscreen mode Exit fullscreen mode

Поскольку первый параметр createClock имеет тип ClockConstructor, в createClock (AnalogClock, 7, 32) он проверяет, имеет ли AnalogClock правильную сигнатуру конструктора.

Другой простой способ - использовать выражения классов:

interface ClockConstructor {
  new (hour: number, minute: number);
}

interface ClockInterface {
  tick();
}

const Clock: ClockConstructor = class Clock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("beep beep");
  }
};
Enter fullscreen mode Exit fullscreen mode

Расширение интерфейсов

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

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;
Enter fullscreen mode Exit fullscreen mode

Гибридные типы

Как мы упоминали ранее, интерфейсы могут описывать более сложные типы, присутствующие в реальном мире 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;
Enter fullscreen mode Exit fullscreen mode

Расширение классов интерфейсами

Когда тип интерфейса расширяет тип класса, он наследует переменные класса, но не их реализации. Это как если бы интерфейс объявил всех переменных класса без предоставления реализации. Интерфейсы наследуют даже private и protected переменные базового класса. Это означает, что когда вы создаете интерфейс, который расширяет класс private или protected полями, этот тип интерфейса может быть реализован только этим классом или его подклассом.

Это полезно, когда у вас большая иерархия наследования, но вы хотите указать, что ваш код работает только с подклассами, которые имеют определенные свойства. Подклассы не должны быть связаны, кроме наследования от базового класса. Например:

class Control {
  private state: any;
}

interface SelectableControl extends Control {
  select(): void;
}

class Button extends Control implements SelectableControl {
  select() {}
}

class TextBox extends Control {
  select() {}
}

class ImageControl implements SelectableControl {
// Class 'ImageControl' incorrectly implements interface 'SelectableControl'. Types have separate declarations of a private property 'state'.

  private state: any;
  select() {}
}
Enter fullscreen mode Exit fullscreen mode

В приведенном выше примере SelectableControl содержит все члены Control, включая свойство private state. Поскольку state является private полем, только потомки Control могут реализовать SelectableControl. Это связано с тем, что только потомки элемента Control будут иметь закрытый элемент, созданный в той же декларации, что является обязательным требованием для совместимости закрытых членов.

Top comments (0)