Я изучаю TypeScript для расширения интерфейса. Я непреднамеренно понял, что TypeScript позволяет расширять интерфейс из нескольких классов. Это меня удивило, и я много исследовал, чтобы найти больше информации, но до сих пор не мог ответить на вопросы.
Мне интересно: хотя TypeScript предотвращает наследование классов от нескольких классов (класс имеет 2 или более прямых базовых класса), почему он позволяет одному интерфейсу расширяться непосредственно из нескольких классов, как в этом коде ниже:
class Student{
name:string = ''
}
class Employee {
salary:number =0
}
interface Work_Study extends Student,Employee{
description:string;
}
«один интерфейс, который расширяется непосредственно из нескольких классов, как в коде ниже» — это не так. Интерфейс расширяет несколько типов — неважно, как эти типы были определены. Они могут быть объявлены с помощью class
, interface
или type
. Интерфейс не заботится (или не знает) об определениях (реализациях) методов class
.
«Часто ли оно используется как альтернатива множественному наследованию классов» — нет. Обратите внимание, что ваш Work_Study_Student
не является ни подклассом Student
, ни подклассом Employee
— он не наследует их методы и instanceof
вернет false. Все, что он делает, это объявляет, что он совместим с некоторыми типами.
Пожалуйста, отредактируйте это, чтобы задать один основной вопрос, иначе он рискует закрыться со словами «Требуется больше внимания: в настоящее время этот вопрос включает несколько вопросов в один. Он должен быть сосредоточен только на одной проблеме».
Вы согласны с тем, что interface Foo {a: string}; interface Bar {b: number}; interface Baz extends Foo, Bar {}
разрешен в TypeScript? Или тебе это тоже нужно объяснить? Мне интересно, вопрос в том, «почему TS вообще разрешает множественное наследование в системе типов» или конкретно «почему TS допускает множественное наследование от типов экземпляров классов»? У них разные ответы; пожалуйста отредактируйте, чтобы уточнить, какой из этих вопросов вы задаете.
@Bergi, по вашему мнению, все, что происходит за кулисами, когда интерфейс расширяется из нескольких классов, — это просто копировать структуру (сигнатуры, если члены являются методами) членов во всех классах (которые расширяются интерфейсом), а затем объединять ее, чтобы создать новый тип контакта (интерфейс). и это верно, когда класс заменяется интерфейсом или псевдонимом типа?
@jcalz Я согласен с тем, что interface Foo {a: string}; interface Bar {b: number}; interface Baz extends Foo, Bar {}
разрешено в машинописном тексте, и это просто копирование структуры Foo и Bar, объединение двух структур для создания нового контракта, а именно Baz. Но я не уверен, что когда интерфейс расширяет класс, можете ли вы помочь мне прояснить это?
Возможно, вы хотите отредактировать примерно так: «Что означает, когда интерфейс расширяет класс в TypeScript?». Ответ выглядит так: «class X {}
вводит в область видимости две вещи с именем X
. Одна из них — это значение конструктора класса JS X
, существующее во время выполнения, а другая — тип интерфейса экземпляра класса X
, который существует только в системе типов TS. Последнее — это то, что вы ссылаются на interface Y extends X {}
. Таким образом, вы можете свободно расширять несколько типов экземпляров классов, поскольку они являются просто интерфейсами. Вы не трогаете классы во время выполнения.
@jcalz, что такое тип интерфейса экземпляра класса? какова цель машинописного текста? можешь прояснить ситуацию? Поскольку я впервые слышу это ключевое слово, я быстро поискал в Google, но не нашел ответа.
Я имею в виду, что если вы напишете declare class X {y: string; z: number}
, оно будет вести себя очень похоже на interface X { y: string; z: number }; declare const X: new () => X;
. Объявление класса объявляет тип, подобный интерфейсу, соответствующий типу экземпляра класса, и константное значение, соответствующее конструктору. Тип экземпляра не является в буквальном смысле интерфейсом, но ведет себя почти точно так же. И когда вы пишете interface W extends X {}
, это X
— это тип, а не значение. Это проясняет ситуацию? Я рад написать ответ, но сначала хочу убедиться, что он применим.
@jcalz Когда я пишу interface W extends X, Y, Z... {}
в этих X
, Y
, `Z`, они могут быть интерфейсом, классом или даже псевдонимом типа?
@jcalz Ваше объяснение очень очевидно. Прежде чем вы напишете ответ, мне интересно следующее: когда класс расширяется class A {}; class B extend A {}
, машинописный текст будет делать 2 вещи: первое, что нужно сделать, это расширить тип интерфейса экземпляра A с помощью типа интерфейса B, например interface B extend A {}
. Второе, что он делает, это работает наследование (наследование [[Prototype]]), которое связано со значениями конструктора и временем выполнения (действие javascript). это правда?
@jcalz Могу ли я заявить, что: «В машинописном скрипте наследованием считается только расширение класса, расширение интерфейса — это просто структура типа слияния для создания нового контракта (типа интерфейса)».
Ваше резюме во многом верно, но я бы не стал использовать такую терминологию. Я бы сказал, что interface B extends A {}
— это тоже «наследование», но это наследование типов, и с множественным наследованием проблем нет, потому что нет необходимости беспокоиться о реализации. Множественное наследование реализаций потенциально может сбить с толку, поскольку тогда что-то должно выбирать, от какой реализации наследовать. Но когда речь идет только о типах, вы можете легко наследовать оба типа. Итак, могу ли я написать ответ сейчас? Мы на одной волне? Или я еще что-то упускаю.
@jcalz три резюме верны?
@jcalz Спасибо за объяснение, оно действительно потрясающее. Не могли бы вы написать ответ?
Я сделаю это скоро.
В целом использование наследования часто считается плохой практикой. Есть несколько хороших практик, которые вам следует запомнить:
Принцип разделения интерфейса (ISP): «Ни один код не должен зависеть от методов, которые он не использует».
Принцип повторного использования композиции (CRP): «Классы должны отдавать предпочтение полиморфному поведению и повторному использованию кода по своей композиции (путем содержания экземпляров других классов, реализующих желаемую функциональность), а не наследованию от базового или родительского класса».
Вам не разрешено использовать множественное наследование, чтобы избежать проблемы ромба и побудить разработчиков придерживаться принципа CRP. Это дизайнерское решение комитета ECMAScript относительно наследования в JavaScript.
О вашем вопросе 1. Вам разрешено расширять несколько интерфейсов в соответствии с требованиями интернет-провайдера. Например:
interface Disposable {
dispose: () => void;
}
interface Serializable {
serialize: () => string;
}
class Student implements Disposable, Serializable {
name:string = '';
somethingElse: number = 0;
serialize() {
return JSON.stringify(this);
}
dispose() {
// dispose logic
}
}
Если бы вам разрешили реализовать только один интерфейс, вы бы побудили разработчиков нарушить принцип ISP.
Использование нескольких интерфейсов означает, что при реализации других частей вашего приложения эти части не нужно связывать с Student
; они могут быть связаны только с теми методами, которые им нужны:
function cleanUp(something: Disposable) {
something.dispose();
}
function serialzie(something: Serializable) {
return something.serialize();
}
По поводу ваших вопросов 2 и 3:
Да, реализация нескольких интерфейсов является обычным явлением. Вот пример в самом коде TypeScript.
Да, то же самое, что и 2. Есть только одно ограничение — проблема с алмазом:
interface Student {
name:string;
somethingElse: number;
}
interface Employee {
salary:number;
somethingElse: string;
}
// 'WorkStudy' cannot simultaneously extend types 'Student' and 'Employee'.
// Named property 'somethingElse' of types 'Student' and 'Employee' are not identical.
interface WorkStudy extends Student, Employee{
description:string;
}
Если вы хотите повторно использовать код, единственным вариантом будет использование композиции вместо наследования:
interface IStudent {
name:string;
}
interface IEmployee {
salary:number;
}
class Student implements IStudent {
constructor(public name: string) {}
}
class Employee implements IEmployee {
constructor(public salary: number) {}
}
class WorkStudy implements IStudent, IEmployee{
private _student: IStudent;
private _employee: IEmployee;
constructor(
public description: string,
student: IStudent,
employee: IEmployee
) {
this._employee = employee;
this._student = student;
}
get salary() {
return this._employee.salary;
}
get name() {
return this._student.name;
}
}
Итак, ваш единственный вариант — придерживаться CRP.
«Это дизайнерское решение команды TypeScript» — а, нет? Почему ты это пишешь?
Когда вы реализуете язык программирования, вы как разработчик можете разрешить или запретить множественное наследование. В этом случае они предпочитают его не поддерживать. Классы JS имеют единое наследование, но нужно помнить, что TypeScript был реализован задолго до того, как в JavaScript появились классы. Запрет множественного наследования всегда является наиболее вероятным случаем, поскольку существуют документированные проблемы с противоположным сайтом.
TypeScript был основан на предложениях классов ES6 и до них не был реализован. В JS всегда было единое наследование, и он никогда не предполагал множественное наследование, поэтому дизайнеры TypeScript были в значительной степени ограничены этим; это было не их решение.
Честно говоря, я перешел в комитет ECMAScript. Я пытался подчеркнуть, что наличие множественного или единственного наследования не имеет никакой «причины», кроме выбора дизайнеров.
Если бы TypeScript поддерживал множественное наследование классов, проблема ромба была бы возможна.
class A {
f() {
// Some implementation
}
}
class B extends A {
f() {
// Overridden implementation of A.f()
}
}
class C extends A {
f() {
// Overridden implementation of A.f()
}
}
class D extends B, C {
// Do not override the implementation of f()
}
let d: D = new D();
d.f(); // It is not clear which function to call (from A, B, or C).
В случае наследования интерфейса такой проблемы не возникает.
class A {
f() {
// Some implementation
}
}
class B {
f() {
// Some implementation
}
}
interface C extends A, B {
}
// You CANNOT instantiate an interface!
// let c:C = new C();
// You must create an implementation of the С
class CImpl implements C {
f(): void {
// And you are required to implement f().
}
}
let c:C = new CImpl();
// In this case, there is no ambiguity as in the case of class inheritance.
c.f();
Когда вы пишете объявление класса в TypeScript, например
class Student {
name: string = ''
}
он включает в себя две вещи с именем Student
. Одним из них является значение конструктора класса, которое существует во время выполнения JavaScript. Это объект с именем Student
, и вы можете написать new Student()
, чтобы создать новое значение экземпляра класса. Другой — тип экземпляра класса, который существует только в системе типов TypeScript. Это тип с именем Student
, и вы можете написать let student: Student
, чтобы сообщить TypeScript, что тип student
является типом экземпляра класса Student
. Несмотря на то, что они имеют одно и то же имя и связаны между собой, это не одно и то же.
TypeScript не запутается в том, о чем вы говорите, поскольку значения и типы живут в разных синтаксических контекстах TypeScript. Если вы напишете x = new X()
, это X
определенно является значением, а не типом. Если вы напишете let x: X;
, то это определенно тип, а не значение. Если вы напишете X
, то это определенно тип, а не значение.
Дополнительную информацию см. в документации по TypeScript.
Также важно отметить, что тип экземпляра класса ведет себя точно так же, как интерфейс . Он не объявлен с использованием ключевого слова interface Z extends X {}
, но ведет себя точно так же. Действительно, приведенное выше объявление X
очень похоже на пару
interface Student {
name: string
}
const Student: new () => Student = class {
name: string = ""
};
где две вещи с именем interface
определены явно и отдельно.
Если мы соединим их вместе, мы сможем понять, что
class Student {
name: string = ''
}
class Employee {
salary: number = 0
}
interface Work_Study extends Student, Employee {
description: string;
}
делает. Объявление Student
с предложением расширения просто объявляет новый интерфейс, который наследуется от типов Student
и interface
. Это не имеет никакого отношения к конструкторам Student
и Employee
. Это точно так же, как
interface Student {
name: string
}
interface Employee {
salary: number;
}
interface Work_Study extends Student, Employee {
description: string;
}
И множественное наследование интерфейсов не представляет проблем.
С другой стороны, что-то вроде
class Work_Study extends Student, Employee {
description: string = "oops"
}
отличается тем, что это объявление класса и включает в себя пространство значений. Это неверно, потому что это предложение расширения JavaScript , которое позволяет расширять только один родительский класс. Множественное наследование в классах JavaScript запрещено (во избежание проблемы ромба, когда неясно, какую реализацию использовать).
Хотя Student
и Employee
выглядят одинаково, они находятся в совершенно разных синтаксических контекстах и ведут себя по-разному. Первое является незаконным множественным наследованием классов, а второе — допустимым множественным наследованием интерфейсов.
По сути, это переработка существующих типов, вариант использования будет зависеть от того, чего и как вы пытаетесь достичь. Это действительно довольно часто используется.