Интерфейсы
Эта страница руководства была заменена, перейдите на новую страницу
Одним из основных принципов 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:
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, реализует этот интерфейс, как это может потребоваться в других языках. Здесь важна только форма. Если объект, который мы передаем функции, удовлетворяет перечисленным требованиям, то это допускается.
Стоит отметить, что проверяющий типов не требует, чтобы эти свойства были в каком-либо порядке, только что требуемые интерфейсом свойства были присутствуют и имели требуемый тип.
Необязательные свойства
Не все свойства интерфейса могут быть обязательными. Некоторые существуют при определенных условиях или могут отсутствовать вообще. Эти необязательные свойства популярны при создании шаблонов, таких как «пакеты параметров», где вы передаёте объект функции, в которой заполнены только несколько свойств.
Вот пример такого шаблона:
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" }); Интерфейсы с необязательными свойствами пишутся аналогично другим интерфейсам, при этом каждое необязательное свойство обозначается символом ? в конце имени свойства в объявлении.
Преимущество необязательных свойств заключается в том, что вы можете описать эти потенциально доступные свойства, одновременно предотвращая использование свойств, которые не являются частью интерфейса. Например, если бы мы неправильно написали имя свойства color в createSquare, мы получили бы сообщение об ошибке, информирующее нас о:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = { color: "white", area: 100 };
if (config.clor) {
// Error: Property 'clor' does not exist on type 'SquareConfig'
newSquare.color = config.clor;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({ color: "black" }); Только для чтения свойства
Некоторые свойства должны изменяться только при первом создании объекта. Вы можете указать это, поместив readonly перед именем свойства:
interface Point {
readonly x: number;
readonly y: number;
} Вы можете создать экземпляр Point, присвоив литерал объекта. После присвоения x и y изменить нельзя.
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error! TypeScript поставляется с типом ReadonlyArray<T>, который идентичен типу Array<T>, но со всеми методами изменения удалены, так что вы можете убедиться, что вы не изменяете массивы после создания:
let a: number[] = [1, 2, 3, 4]; let ro: ReadonlyArray<number> = a; ro[0] = 12; // error! ro.push(5); // error! ro.length = 100; // error! a = ro; // error!
В последней строке фрагмента вы можете увидеть, что даже присвоение всего массива ReadonlyArray обратно обычному массиву является незаконным. Однако вы всё равно можете переопределить его с помощью явного приведения типа:
let a: number[] = [1, 2, 3, 4]; let ro: ReadonlyArray<number> = a; a = ro as number[];
readonly против const
Самый простой способ запомнить, нужно ли использовать readonly или const, — спросить, используете ли вы его для переменной или свойства. Для переменных используется const, а для свойств — readonly.
Проверка лишних свойств
В нашем первом примере с интерфейсами TypeScript позволяет нам передать { size: number; label: string; } тому, кто ожидал только { label: string; }. Мы также только что узнали об необязательных свойствах и о том, как они полезны при описании так называемых «пакетов параметров».
Однако, наивное сочетание этих двух может привести к ошибке. Например, возьмём наш последний пример с createSquare:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
return {
color: config.color || "red",
area: config.width ? config.width * config.width : 20,
};
}
let mySquare = createSquare({ colour: "red", width: 100 }); Обратите внимание, что переданный аргумент функции createSquare написан colour вместо color. В обычном JavaScript такое поведение никак не проявляется.
Можно утверждать, что эта программа правильно типизирована, поскольку свойства width совместимы, нет свойства color, а дополнительное свойство colour несущественно.
Однако TypeScript придерживается точки зрения, что в этом коде, вероятно, есть ошибка. Литералы объектов получают специальное обращение и проходят проверку лишних свойств при присваивании их другим переменным или передаче в качестве аргументов. Если у объекта-литерала есть свойства, которых нет в «целевом типе», вы получите ошибку:
let mySquare = createSquare({ colour: "red", width: 100 }); Обход этих проверок на самом деле очень прост. Самый простой метод — использовать явное приведение типа:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig); Однако, лучшим подходом может быть добавление подписи индекса строк, если вы уверены, что объект может иметь дополнительные свойства, которые используются каким-то особым образом. Если SquareConfig может иметь свойства color и width с указанными типами, но также может иметь любое количество других свойств, то мы могли бы определить его так:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
} Мы обсудим подписи индексов позже, но здесь мы говорим, что SquareConfig может иметь любое количество свойств, и если они не color или width, их типы не важны.
Ещё один способ обойти эти проверки, который может быть немного неожиданным, — присвоить объект другой переменной: поскольку squareOptions не будет проходить проверку лишних свойств, компилятор не выдаст ошибку.
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions); Этот обходной путь будет работать, пока у squareOptions и SquareConfig есть общее свойство. В данном примере это свойство width. Однако он не сработает, если у переменной нет ни одного общего свойства объекта. Например:
let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions); Помните, что для простого кода, как выше, вы, вероятно, не должны пытаться «обходить» эти проверки. Для более сложных литералов объектов, имеющих методы и хранящих состояние, вам, возможно, нужно будет учитывать эти методы, но большинство ошибок проверки лишних свойств — это на самом деле ошибки. Это означает, что если вы сталкиваетесь с проблемами проверки лишних свойств для чего-то вроде пакетов параметров, вам, возможно, придётся пересмотреть некоторые ваши объявления типов. В этом случае, если допустимо передавать объект с обоими свойствами color или colour функции createSquare, вы должны исправить определение SquareConfig так, чтобы это отражалось.
Типы функций
Интерфейсы способны описывать широкий спектр форм, которые могут принимать объекты JavaScript. Помимо описания объекта со свойствами, интерфейсы также могут описывать типы функций.
Чтобы описать тип функции с помощью интерфейса, мы даем интерфейсу сигнатуру вызова. Это похоже на объявление функции, в котором указаны только список параметров и тип возвращаемого значения. Каждый параметр в списке параметров требует как имя, так и тип.
interface SearchFunc {
(source: string, subString: string): boolean;
} После определения мы можем использовать этот интерфейс типа функции, как мы бы использовали другие интерфейсы. Здесь показано, как создать переменную типа функции и присвоить ей значение функции того же типа.
let mySearch: SearchFunc;
mySearch = function (source: string, subString: string): boolean {
let result = source.search(subString);
return result > -1;
}; Для корректной проверки типов функций имена параметров не обязательно должны совпадать. Мы могли бы, например, написать вышеприведенный пример так:
let mySearch: SearchFunc;
mySearch = function (src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}; Параметры функций проверяются по одному, при этом тип каждого соответствующего параметра проверяется по отношению друг к другу. Если вы не хотите указывать типы вообще, контекстная типизация TypeScript может вывести типы аргументов, поскольку значение функции непосредственно присваивается переменной типа SearchFunc. Здесь также тип возвращаемого значения нашего выражения функции подразумевается значениями, которые оно возвращает (здесь false и true).
let mySearch: SearchFunc;
mySearch = function (src, sub) {
let result = src.search(sub);
return result > -1;
}; Если бы выражение функции возвращало числа или строки, проверяющий типов выдал бы ошибку, указывающую, что тип возвращаемого значения не соответствует типу возвращаемого значения, описанному в интерфейсе SearchFunc.
let mySearch: SearchFunc;
mySearch = function (src, sub) {
let result = src.search(sub);
return "string";
}; Индексируемые типы
Так же, как мы можем использовать интерфейсы для описания типов функций, мы также можем описать типы, в которые мы можем «индексировать», например, a[10] или ageMap["daniel"]. Индексируемые типы имеют подпись индекса, которая описывает типы, которые мы можем использовать для индексирования объекта, а также соответствующие типы возвращаемых значений при индексировании.
Давайте рассмотрим пример:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0]; Выше мы имеем интерфейс StringArray, который имеет подпись индекса. Эта подпись индекса указывает, что при индексировании StringArray с number, будет возвращено значение типа string.
Существует четыре типа поддерживаемых подписей индексов: строка, число, символ и шаблоны строк. Возможно поддерживать множество типов индексаторов, но тип, возвращаемый числовым индексатором, должен быть подтипом типа, возвращаемого строковым индексатором.
Это происходит потому, что при индексировании с number, JavaScript фактически преобразует его в string перед индексированием в объект. Это означает, что индексирование с 100 (числом) такое же, как индексирование с "100" (строкой), поэтому они должны быть согласованы.
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
} Хотя строковые подписи индексов — мощный способ описать шаблон «словарь», они также накладывают требование, чтобы все свойства соответствовали своему типу возвращаемого значения. Это происходит потому, что строковый индекс заявляет, что obj.property также доступен как obj["property"]. В следующем примере тип name не соответствует типу строкового индекса, и проверяющий типов выдаёт ошибку:
interface NumberDictionary {
[index: string]: number;
length: number; // ok, length is a number
name: string; // error, the type of 'name' is not a subtype of the indexer
} Однако свойства разных типов допустимы, если подпись индекса является объединением типов свойств:
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
} Наконец, вы можете сделать подпись индекса readonly для предотвращения присвоения их индексов:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error! Вы не можете установить myArray[2], потому что подпись индекса readonly.
Индексируемые типы со строками шаблонов
Строка шаблона может использоваться для указания, что определённый шаблон разрешён, но не все. Например, объект HTTP-заголовков может иметь список известных заголовков и поддерживать любые пользовательские свойства, которые начинаются с x-.
interface HeadersResponse {
"content-type": string,
date: string,
"content-length": string
// Permit any property starting with 'x-'.
[headerName: `x-${string}`]: string;
}
function handleResponse(r: HeadersResponse) {
// Handle known, and x- prefixed
const type = r["content-type"]
const poweredBy = r["x-powered-by"]
// Unknown keys without the prefix raise errors
const origin = r.origin
} Типы классов
Реализация интерфейса
Одно из наиболее распространённых применений интерфейсов в языках, таких как C# и Java, заключающееся в явном обеспечении соответствия класса определённому контракту, также возможно в TypeScript.
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
constructor(h: number, m: number) {}
} Вы также можете описать методы в интерфейсе, которые реализованы в классе, как мы делаем с setTime в приведённом ниже примере:
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) {}
} Интерфейсы описывают публичную часть класса, а не публичную и приватную. Это запрещает вам использовать их для проверки того, что класс также имеет определённые типы для приватной части экземпляра класса.
Разница между статической и экземплярной сторонами классов
Работая с классами и интерфейсами, полезно помнить, что класс имеет два типа: тип статической части и тип экземплярной части. Вы можете заметить, что если вы создаёте интерфейс с конструкторским сигнатуром и пытаетесь создать класс, который реализует этот интерфейс, то получите ошибку:
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) {}
} Это связано с тем, что когда класс реализует интерфейс, проверяется только экземплярная сторона класса. Так как конструктор находится в статической части, он не включён в эту проверку.
Вместо этого вам нужно будет работать напрямую со статической частью класса. В этом примере мы определяем два интерфейса, 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); Так как первый параметр createClock имеет тип ClockConstructor, в createClock(AnalogClock, 7, 32), он проверяет, что AnalogClock имеет правильный конструкторский сигнатур.
Ещё один простой способ — использовать выражения класса:
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick(): void;
}
const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
};
let clock = new Clock(12, 17);
clock.tick(); Расширение интерфейсов
Как и классы, интерфейсы могут расширять друг друга. Это позволяет копировать члены одного интерфейса в другой, что обеспечивает большую гибкость в том, как вы разбиваете свои интерфейсы на повторно используемые компоненты.
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = {} as Square;
square.color = "blue";
square.sideLength = 10; Интерфейс может расширять несколько интерфейсов, создавая комбинацию всех интерфейсов.
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 вы иногда можете столкнуться с объектом, который работает как комбинация некоторых из описанных выше типов.
Один из таких примеров — объект, который действует как функция и объект одновременно, с дополнительными свойствами:
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; При взаимодействии с JavaScript сторонних разработчиков вам может потребоваться использовать такие схемы, как выше, чтобы полностью описать форму типа.
Интерфейсы, расширяющие классы
Когда тип интерфейса расширяет тип класса, он наследует члены класса, но не их реализации. Это как если бы интерфейс объявлял все члены класса без предоставления реализации. Интерфейсы наследуют даже приватные и защищённые члены базового класса. Это означает, что когда вы создаёте интерфейс, который расширяет класс с приватными или защищёнными членами, этот тип интерфейса может быть реализован только этим классом или его подклассом.
Это полезно, когда у вас есть иерархия наследования большой объёмом, но вы хотите указать, что ваш код работает только с подклассами, которые имеют определённые свойства. Подклассы не обязаны быть связаны, кроме наследования от базового класса. Например:
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 {
private state: any;
select() {}
} В приведённом выше примере SelectableControl содержит все члены Control, включая приватное свойство state. Так как state является приватным членом, реализовать SelectableControl могут только потомки Control. Это связано с тем, что только потомки Control будут иметь приватный член state, который берёт своё начало от того же объявления, что и требование для совместимости приватных членов.
Внутри класса Control можно получить доступ к приватному члену state через экземпляр SelectableControl. По сути, SelectableControl действует как Control, который известен тем, что имеет метод select. Классы Button и TextBox являются подтипами SelectableControl (потому что оба наследуют от Control и имеют метод select). Класс ImageControl имеет собственный приватный член state вместо расширения Control, поэтому он не может реализовать SelectableControl.
© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/interfaces.html