[TypeScript] Интерфейсы
Tags: typescript, tutorial, webdev
Published: 2020-05-18 06:31:20

Вольный перевод документации Typescript. [Interfaces](https://www.typescriptlang.org/docs/handbook/interfaces.html)

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

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

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

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

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
```

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

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

```typescript
interface LabeledValue {
  label: string;
}

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

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
```

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

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

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

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

```typescript 
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" });
```

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

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

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

```typescript 
interface Point {
  readonly x: number;
  readonly y: number;
}
```

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

```typescript
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[]'.
```

## Типы функций

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

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

```typescript
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;
};
```

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

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

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

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

let myStr: string = myArray[0];
```

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

## Типы классов 

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

```typescript 
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) {}
}
```

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

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

```typescript 
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) {}
}
```

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

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

```typescript
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);
```

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

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

```typescript
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");
  }
};
```

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

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

```typescript
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;
```

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

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

Одним из таких примеров является объект, который действует как функция и объект с дополнительными свойствами:

```typescript
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;
```

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

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

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

```typescript 
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() {}
}
```

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