Как расширить TypeScript-интерфейс из библиотеки, если она была импортирована под другими именем?
Определения типов TypeScript к библиотеке Vinyl позволяют нам добавлять новые свойства к экземпляру File , потому при самых строгих настройках TypeScript не будет на это ругаться:
interface File < contents: Buffer | NodeJS.ReadableStream | null; // . [customProperty: string]: any; >
Всё замечательно, но без автодополнений, которые предоставляют современные интегрированные среды разработки (например, IntelliJ IDEA), повышается вероятность ошибки ввода, а TypeScript её не заметит, потому что нам позволено добавлять сколько угодно новых свойств к File . На данный момент предлагаются автодополнения только тех свойств, которые указаны в @types/vynil/index.d.ts. Вероятно, если расширить интерфейс File , который даёт Vynil, до среда разработки его заметит и начнёт предлагать также пользовательские свойства в качестве автодополнений. Это вопрос назывался бы «как расширить TypeScript интерфейс сторонней библиотеки», если не одно «но»: поскольку интерфейс File уже занят типами к стандартным NodeJS-модулям, то я импортирую не import File from ‘vinyl’; , а import Vinyl from ‘vinyl’; . Это даёт возможность аннотировать Vynil-экземпляры как Vynil , а не File :
// . return gulp.src(entryPointsSourceFilesAbsolutePaths) .pipe(this.gulpPlugins.intercept((fileInInitialState: Vinyl) => < fileInInitialState.selfSourceVersionAbsolutePath = fileInInitialState.path; return fileInInitialState; >)) // .
Итак, где, под каким именем и каким образом я должен расширить TypeScript-интерфейс библиотеки Vylil, что бы с точки зрения TypeScript-а это было правильное расширение интерфейса? (Тогда IDE с большой вероятностью начнёт показывать автодополнения и для моих свойств, в том числе selfSourceVersionAbsolutePath из примера выше).
Фрагмент из определений к vynil с комментарием
/// import * as fs from 'fs'; interface ConstructorOptions < // .. >interface FileConstructor < // . prototype: File; >export = File; declare let File: FileConstructor; interface File < // . >
Интерфейсы в TypeScript
Что такое интерфейсы и зачем они вообще нужны? TypeScript ввел новый тип данных, определяемый с помощью конструкции называемой «interface» . Интерфейсы в TypeScript служат для именования типов данных, и являются способом определения соглашений внутри кода. Другими словами, создавая интерфейс мы создаем некоторый тип данных, который в основном служит для объектов или классов, где мы указываем какие поля, какие функции и какие вообще элементы должны присутствовать у этих объектов. Давайте рассмотрим вот такой простой пример:
interface IUser < readonly id: number, name: string, color?: string, size:< width: number, height: number >>
Некоторые свойства должны изменяться только один раз, при создании объекта. Этого можно добиться, ограничив свойство только для чтения, применив модификатор “readonly” , как мы с делали это полем “id” .
Не всегда все свойства интерфейса должны являться обязательными. Сделать свойство необязательным для заполнения путем добавления знака “?” после его именования, как это сделано у поля “color” в примере.
Также в интерфейсе можно указывать, какие-то другие объекты, как это сделано у поля “size” .
После создания интерфейса, для нас открывается возможность создавать различные объекты которые будут являться типом нашего интерфейса, а именно “IUser” . Давайте
посмотрим на примере как это работает:
const user: IUser = < id: 123, name: 'Чак Норрис', color: '#ccc', size:< width: 20, height: 30 >>
В примере мы создали переменную “user” , указав ее тип “IUser” . В следующем примере мы проделаем подобные действия, но уже не добавляя свойство “color” , так как оно указано как не обязательное, но добавляя его ниже созданной переменной.
const user: IUser = < id: 123, name: 'Чак Норрис', size:< width: 20, height: 30 >> user.color = “red”
Единственное, что здесь может смутить, это то что переменная “user” , является константой, а в последствии мы ее изменяем. Но в конечном итоге, так как, мы работаем в JavaScript, то мы можем изменять внутреннее состояние констант, если они является объектом или массивом.
Так же мы можем указывать к какому типу будет относиться объект. Вот два примера:
const user = <> as IUser const user1 = <> // более старая запись
Наследование (или расширение) интерфейсов
TypeScript позволяет, создавать интерфейсы включающие в себя комбинации других интерфейсов, что позволяет настроить очень гибкое взаимодействие между интерфейсами. Давайте рассмотрим это на примере:
interface IUserWidthArea extends IUser < getArea:()=>number > const user2: IUserWidthArea = < id: 123, name: 'Чак Норрис', size:< width: 20, height: 30 >, getArea(): number < return this.size.width * this.size. height >>
Интерфейс, который мы создали будет наследоваться от интерфейса “IUser” . В него мы можем добавлять какие-то новые поля. В нашем случае мы обязываем его реализовывать функцию “getArea()” , которая будет рассчитывать площадь.
Так же интерфейсы могут взаимодействовать с классами. Давайте
это рассмотрим на примере интерфейса “IClock” :
interface IClock < time: Date, setTime(date: Date): void >сlass Clock implements IClock < time: Date = new Date() setTime(date: Date): void< this.time = date >>
Еще что бы я хотел рассказать об интерфейса в рамках это статья, это то что бывают ситуации, когда необходимо создать интерфейс для объекта у которого будет большое количество динамических ключей, например:
const css = < padding: '10px 40px 10px 40px', position: 'relative', margin: '10px 60px 10px 60px', height: '90px', // и так далее >
Создавая интерфейс для данный переменной мы не можем перечислить всевозможные свойства, это будет крайне неэффективно, поэтому для таких ситуаций существует специальный синтаксис:
interface IStyles
Где в квадратных скобках мы указываем тип ключа и тип его значения.
Как расширить интерфейс typescript
Интерфейс определяет свойства и методы, которые объект должен реализовать. Другими словами, интерфейс — это определение кастомного типа данных, но без реализации. В данном случае интерфейсы в TS похожи на интерфейсы в языках Java и C#. Интерфейсы определяются с помощью ключевого слова interface . Для начала определим простенький интерфейс:
interface IUser
Интерфейс в фигурных скобках определяет два свойства: 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 =
Свойство 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(`$говорит "$"`); > > 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: $name: $ 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 = 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 ), реализует эту функцию конструктора, и также может использовать другие свойства и методы, которые были определены в интерфейсе.
Введение #
Одним из основных принципов TypeScript является то, что проверка типов основывается на форме значений. Этот подход иногда называется «утиной типизацией» либо «структурным подтипированием». В TypeScript интерфейсы выполняют функцию именования типов, и являются мощным способом определения соглашений внутри кода, а также за пределами проекта.
Наш первый интерфейс #
Самый простой способ увидеть, как работают интерфейсы — начать с простого примера:
function printLabel(labelledObj: < label: string >) < console.log(labelledObj.label); >let myObj = ; printLabel(myObj);
Компилятор проверяет вызов printLabel . Эта функция принимает один параметр, который требует, чтобы у переданного объекта было свойство под именем label , которое имело бы строковый тип. Обратите внимание, что у нашего объекта есть и другие свойства, однако компилятор проверяет лишь то, что у него есть, по крайней мере, необходимые свойства, и их типы совпадают с требуемыми. В некоторых случаях, которые мы рассмотрим чуть позже, TypeScript ведет себя не так снисходительно.
Мы можем переписать этот пример, на этот раз используя интерфейс для того, чтобы отразить необходимость наличия свойства label строкового типа:
interface LabelledValue < label: string; >function printLabel(labelledObj: LabelledValue) < console.log(labelledObj.label); >let myObj = ; printLabel(myObj);
Интерфейс LabelledValue — это имя, которое теперь можно использовать, чтобы задать требование из предыдущего примера. Он по-прежнему отражает необходимость того, что объект должен иметь свойство строкового типа с именем label . Обратите внимание — совсем не обязательно явно указывать, что объект, который мы передаем в printLabel , реализует данный интерфейс, как пришлось бы делать в иных языках. В TypeScript имеет значение только форма объекта. Если объект, который передается в функцию, удовлетворяет перечисленным требованиям, то он считается подходящим.
Стоит отметить, что проверка типов не требует, чтобы свойства шли в определенном порядке: важно лишь, что необходимые свойства присутствуют и имеют подходящий тип.
Опциональные свойства #
Не все свойства интерфейса могут быть обязательными. Некоторые существуют только в определенных условиях, либо отсутствуют вообще. Такие необязательные (опциональные) свойства часто встречаются, к примеру, при передаче в функцию аргументов в виде объекта, в котором указаны всего несколько свойств.
Вот пример реализации такого приема:
interface SquareConfig < color?: string; width?: number; >function createSquare(config: SquareConfig): < let newSquare = ; if (config.color) < newSquare.color = config.color; >if (config.width) < newSquare.area = config.width * config.width; >return newSquare; > let mySquare = createSquare();
Интерфейсы с необязательными свойствами записываются подобно обычным, но каждое опциональное свойство помечается символом ? в конце имени.
Преимущество опциональных свойств заключается в том, что можно описать свойства, которые, возможно, будут присутствовать, и в то же время запретить использование тех свойств, которые не являются частью интерфейса. К примеру, если бы мы ошиблись при вводе имени color в createSquare , то получили бы сообщение об ошибке, информирующее об этом:
interface SquareConfig < color?: string; width?: number; >function createSquare(config: SquareConfig): < color: string; area: number >< let newSquare = ; if (config.color) < // Ошибка: Property 'collor' does not exist on type 'SquareConfig' newSquare.color = config.collor; >if (config.width) < newSquare.area = config.width * config.width; >return newSquare; > let mySquare = createSquare();
Свойства только для чтения #
Некоторые свойства должны быть изменяемыми только в момент создания объекта. Вы можете указать это, добавив readonly перед его именем:
interface Point
Создать объект Point можно присваиванием объектного литерала, но после присваивания изменить x и y будет больше нельзя.
let p1: Point = < x: 10, y: 20 >; p1.x = 5; // ошибка!
В TypeScript есть тип ReadonlyArray , который, по сути, является типом Array , из которого удалены все изменяющие его методы, так что можно быть уверенным, что такие массивы не будут изменяться после создания:
let a: number[] = [1, 2, 3, 4]; let ro: ReadonlyArray = a; ro[0] = 12; // ошибка! ro.push(5); // ошибка! ro.length = 100; // ошибка! a = ro; // ошибка!
В последней строке примера можно видеть, что даже присваивание ReadonlyArray обычному массиву недопустимо. Впрочем, это ограничение все равно можно обойти, использовав приведение типов:
a = ro as number[];
readonly против const
Самый простой способ запомнить, когда нужно использовать readonly , а когда const — задать вопрос, нужна ли эта возможность для переменной либо для свойства объекта. С переменными используется const , а со свойствами — readonly .
Проверки на лишние свойства #
В нашем первом примере использования интерфейсов TypeScript позволил передать < size: number; label: string; >там, где ожидалось всего лишь < label: string; >. Также мы узнали о необязательных свойствах, и о том, как они могут быть полезны при передаче аргументов в функции.
Однако, бездумное сочетание двух этих возможностей позволило бы выстрелить себе в ногу так же, как и в JavaScript. К примеру, если взять последний пример с createSquare :
interface SquareConfig < color?: string; width?: number; >function createSquare(config: SquareConfig): < color: string; area: number > < // . >let mySquare = createSquare(< colour: "red", width: 100 >);
Обратите внимание, что аргумент, передаваемый в createSquare , записан как colour вместо color . В чистом JavaScript подобные вещи не выдают ошибок, но и не работают так, как хотел бы разработчик.
Можно сказать, что данная программа корректна с точки зрения типов, так как типы свойств width совместимы, color отсутствует, а наличие дополнительного свойства colour не имеет никакого значения.
Однако TypeScript делает предположение, что в этом куске кода есть ошибка. Литералы объектов обрабатываются им по-особенному, и проходят проверку на наличие лишних свойств. Эта проверка делается, когда литералы либо присваиваются другим переменным, либо передаются в качестве аргументов. Если в литерале есть какие-либо свойства, которых нет в целевом типе, то это будет считаться ошибкой.
// ошибка: 'colour' not expected in type 'SquareConfig' let mySquare = createSquare(< colour: "red", width: 100 >);
Обойти такую проверку очень легко. Самый простой способ — использовать приведение типов:
let mySquare = createSquare( < width: 100, opacity: 0.5 >as SquareConfig);
Если же вы уверены, что объект может иметь дополнительные свойства, которые будут использоваться каким-то особенным способом, то есть способ еще лучше — добавить строковый индекс. Если объекты SquareConfig могут иметь свойства color и width , а также любое количество иных свойств, то интерфейс можно описать следующим образом:
interface SquareConfig
Индексы мы обсудим чуть позже, а сейчас просто отметим, что в этом примере SquareConfig может иметь любое количество свойств, и, если это не color и не width , то их тип не имеет значения.
Последний способ обойти проверку на наличие избыточных свойств, который может показаться немножко неожиданным — присваивание объекта другой переменной. Так как squareOptions не будет проходить проверку на избыточные свойства, компилятор не выдаст ошибки.
let squareOptions = < colour: "red", width: 100 >; let mySquare = createSquare(squareOptions);
Не забывайте, что в простом коде, подобном приведенному выше, скорее всего, не нужно пытаться обойти эту проверку. Для более сложных объектных литералов, в которых есть методы, или которые имеют состояние, возможно, придется прибегнуть к помощи этой техники, однако большинство сообщений компилятора, связанных с проверкой на избыточные свойства, указывают на настоящие ошибки. Это значит, что когда вы сталкиваетесь с проблемами, которые порождает такая проверка (к примеру, при передаче в функцию объекта с аргументами), возможно, придется изменить объявления типов. В данном случае, если передача объекта, в котором могут быть одновременно свойства color и colour , приемлема, нужно исправить определение SquareConfig , чтобы отразить это.
Функциональные типы #
Интерфейсы могут описывать широкий диапазон «форм», которые принимают JavaScript-объекты. Кроме описания объектов со свойствами, интерфейсы могут описывать и типы функций.
Для того, чтобы описать функцию с помощью интерфейса, к нему добавляют сигнатуру вызова. Такая сигнатура выглядит как описание функции, в котором указаны только список аргументов и возвращаемый тип. Каждый параметр в списке должен иметь и имя, и тип.
interface SearchFunc
Будучи определенным, такой интерфейс может использоваться так же, как и другие интерфейсы. Сейчас мы покажем, как можно создать переменную функционального типа, и присвоить ей функцию.
let mySearch: SearchFunc; mySearch = function(source: string, subString: string) < let result = source.search(subString); if (result == -1) < return false; >else < return true; >>
Имена параметров не обязательно должны совпадать, чтобы функция прошла проверку на соответствие типов. Мы, к примеру, могли бы записать предыдущий пример вот так:
let mySearch: SearchFunc; mySearch = function(src: string, sub: string): boolean < let result = src.search(sub); if (result == -1) < return false; >else < return true; >>
Параметры функций проверяются друг за другом, и типы параметров, находящихся на соответствующих позициях, сравниваются попарно. Если вы не хотите указывать типы для аргументов, то TypeScript сможет вывести типы из контекста, основываясь на том, что функция присваивается переменной, тип которой — SearchFunc . В следующем примере тип возвращаемого значения функции тоже выводится: это делается на основании значений, которые она возвращает ( false и true ). Если бы функция возвращала числа или строки, то компилятор во время проверки типов предупредил бы, что тип возвращаемого значения не совпадает с типом, указанным в интерфейсе SearchFunc .
let mySearch: SearchFunc; mySearch = function(src, sub) < let result = src.search(sub); if (result == -1) < return false; >else < return true; >>
Индексируемые типы #
Аналогично тому, как интерфейсы используются для описания функций, можно описать типы так, чтобы с ними можно было использовать оператор индекса — например, вот так a[10] или так ageMap[«daniel»] . Индексируемые типы имеют сигнатуру индекса, которая описывает типы, которые можно использовать для индексации объекта, а также типы значений, которые возвращает эта операция. Приведем пример:
interface StringArray < [index: number]: string; >let myArray: StringArray; myArray = ["Bob", "Fred"]; let myStr: string = myArray[0];
Здесь у нас есть интерфейс StringArray , у которого есть сигнатура индекса. Эта сигнатура говорит о том, что когда StringArray индексируется числом, возвращается строка.
Существуют всего два вида поддерживаемых сигнатур индекса: со строками и с числами в качестве аргумента. Объект может поддерживать оба вида, но тип значения, который возвращается числовым индексом, должен быть подтипом того, который возвращается строковым индексом. Так сделано по той причине, что когда операция индекса применяется к объекту, JavaScript сначала преобразует переданное в качестве индекса число в строку. То есть использование индекса 100 (число) — то же самое, что использование «100» (строка), поэтому типы обоих индексов должны согласовываться.
class Animal < name: string; >class Dog extends Animal < breed: string; >// Ошибка: индексация строкой может вернуть объект Dog! interface NotOkay
Кроме того, что строковые индексы — мощный способ описания словарей, они диктуют требование того, чтобы типы всех свойств соответствовали типу, который возвращает операция индекса. Это происходит из-за того, что obj.property доступен и как obj[property] . В следующем примере тип name не совпадает с типом строкового индекса, и компилятор выдает ошибку:
interface NumberDictionary < [index: string]: number; length: number; // все хорошо, length — число name: string; // ошибка, the type of 'name' is not a subtype of the indexer >
Кроме того, сигнатуру индекса можно сделать доступной только для чтения, чтобы запретить присваивание индексам:
interface ReadonlyStringArray < readonly [index: number]: string; >let myArray: ReadonlyStringArray = ["Alice", "Bob"]; myArray[2] = "Mallory"; // ошибка!
Установить myArray[2] нельзя, поскольку сигнатура индекса — только для чтения.
Типы классов #
Реализация интерфейса
В таких языках, как C# и Java интерфейсы наиболее часто используются для того, чтобы явно указать, что класс соответствует определенному соглашению. Это возможно и в TypeScript.
interface ClockInterface < currentTime: Date; >class Clock implements ClockInterface < currentTime: Date; constructor(h: number, m: number) < >>
Также в интерфейсе можно описать методы, которые реализованы внутри класса, как это сделано для setTime в следующем примере:
interface ClockInterface < currentTime: Date; setTime(d: Date); >class Clock implements ClockInterface < currentTime: 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(); >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 Shape < color: string; >interface Square extends Shape < sideLength: number; >let square = <>; square.color = "blue"; square.sideLength = 10;
Интерфейс может расширять сразу несколько других интерфейсов, создавая их комбинацию:
interface Shape < color: string; >interface PenStroke < penWidth: number; >interface Square extends Shape, PenStroke < sideLength: number; >let 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) < >; counter.interval = 123; counter.reset = function () < >; return counter; > let c = getCounter(); c(10); c.reset(); c.interval = 5.0;
При взаимодействии со сторонним кодом может понадобиться использовать подобные приемы, чтобы полноценно описать тип.
Интерфейсы, расширяющие классы #
Когда интерфейс расширяет класс, интерфейс наследует члены класса, но не их реализацию. Это аналогично тому, как если бы интерфейс описывал все члены класса, но не указывал их реализацию. Интерфейсы наследуют даже приватные и защищенные члены базового класса. Это означает, что если создать интерфейс, расширяющий класс с приватными или защищенными членами, то он может быть реализован только самим базовым классом либо его наследниками.
Это полезно в тех случаях, когда существует большая иерархия наследования, и нужно указать, что код работает только с определенными подклассами, у которых есть определенные свойства. Такие подклассы не обязаны иметь отношение друг к другу, кроме того, что они наследуются от одного и того же базового класса. К примеру:
class Control < private state: any; >interface SelectableControl extends Control < select(): void; >class Button extends Control < select() < >> class TextBox extends Control < select() < >> class Image extends Control < >class Location < select() < >>
В этом примере SelectableControl содержит все члены класса Control , включая приватное свойство state . Так как state — приватный член, реализовать интерфейс SelectableControl смогут только наследники Control . Так будет потому, что для совместимости приватных членов необходимо, чтобы они были объявлены в одном и том же базовом классе, а это возможно лишь для наследников Control .
Внутри кода Control можно получить доступ к приватному члену state через экземпляр SelectableControl . По сути, SelectableControl ведет себя так же, как Control , о котором известно, что у него есть метод select . Классы Button и TextBox — подтипы SelectableControl (так как оба унаследованы от Control и у них есть метод select ), однако Image и Location таковыми не являются.
Поддержите перевод документации:
Поддерживатель | Github Репозиторий
Documentation generated by mdoc.