Почему TS допускает множественное наследование интерфейсов?

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

Мне интересно: хотя TypeScript предотвращает наследование классов от нескольких классов (класс имеет 2 или более прямых базовых класса), почему он позволяет одному интерфейсу расширяться непосредственно из нескольких классов, как в этом коде ниже:


    class Student{
        name:string = ''
    }
    
    class Employee {
        salary:number =0
    }
    
    interface Work_Study extends Student,Employee{
        description:string;
    }
    

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

joe 12.08.2024 10:56

«один интерфейс, который расширяется непосредственно из нескольких классов, как в коде ниже» — это не так. Интерфейс расширяет несколько типов — неважно, как эти типы были определены. Они могут быть объявлены с помощью class, interface или type. Интерфейс не заботится (или не знает) об определениях (реализациях) методов class.

Bergi 12.08.2024 11:15

«Часто ли оно используется как альтернатива множественному наследованию классов» — нет. Обратите внимание, что ваш Work_Study_Student не является ни подклассом Student, ни подклассом Employee — он не наследует их методы и instanceof вернет false. Все, что он делает, это объявляет, что он совместим с некоторыми типами.

Bergi 12.08.2024 11:16

Пожалуйста, отредактируйте это, чтобы задать один основной вопрос, иначе он рискует закрыться со словами «Требуется больше внимания: в настоящее время этот вопрос включает несколько вопросов в один. Он должен быть сосредоточен только на одной проблеме».

jcalz 12.08.2024 14:40

Вы согласны с тем, что interface Foo {a: string}; interface Bar {b: number}; interface Baz extends Foo, Bar {} разрешен в TypeScript? Или тебе это тоже нужно объяснить? Мне интересно, вопрос в том, «почему TS вообще разрешает множественное наследование в системе типов» или конкретно «почему TS допускает множественное наследование от типов экземпляров классов»? У них разные ответы; пожалуйста отредактируйте, чтобы уточнить, какой из этих вопросов вы задаете.

jcalz 13.08.2024 05:09

@Bergi, по вашему мнению, все, что происходит за кулисами, когда интерфейс расширяется из нескольких классов, — это просто копировать структуру (сигнатуры, если члены являются методами) членов во всех классах (которые расширяются интерфейсом), а затем объединять ее, чтобы создать новый тип контакта (интерфейс). и это верно, когда класс заменяется интерфейсом или псевдонимом типа?

LeoPkm2-1 13.08.2024 05:10

@jcalz Я согласен с тем, что interface Foo {a: string}; interface Bar {b: number}; interface Baz extends Foo, Bar {} разрешено в машинописном тексте, и это просто копирование структуры Foo и Bar, объединение двух структур для создания нового контракта, а именно Baz. Но я не уверен, что когда интерфейс расширяет класс, можете ли вы помочь мне прояснить это?

LeoPkm2-1 13.08.2024 05:28

Возможно, вы хотите отредактировать примерно так: «Что означает, когда интерфейс расширяет класс в TypeScript?». Ответ выглядит так: «class X {} вводит в область видимости две вещи с именем X. Одна из них — это значение конструктора класса JS X, существующее во время выполнения, а другая — тип интерфейса экземпляра класса X, который существует только в системе типов TS. Последнее — это то, что вы ссылаются на interface Y extends X {}. Таким образом, вы можете свободно расширять несколько типов экземпляров классов, поскольку они являются просто интерфейсами. Вы не трогаете классы во время выполнения.

jcalz 13.08.2024 05:33

@jcalz, что такое тип интерфейса экземпляра класса? какова цель машинописного текста? можешь прояснить ситуацию? Поскольку я впервые слышу это ключевое слово, я быстро поискал в Google, но не нашел ответа.

LeoPkm2-1 13.08.2024 05:46

Я имею в виду, что если вы напишете declare class X {y: string; z: number}, оно будет вести себя очень похоже на interface X { y: string; z: number }; declare const X: new () => X;. Объявление класса объявляет тип, подобный интерфейсу, соответствующий типу экземпляра класса, и константное значение, соответствующее конструктору. Тип экземпляра не является в буквальном смысле интерфейсом, но ведет себя почти точно так же. И когда вы пишете interface W extends X {}, это X — это тип, а не значение. Это проясняет ситуацию? Я рад написать ответ, но сначала хочу убедиться, что он применим.

jcalz 13.08.2024 05:55

@jcalz Когда я пишу interface W extends X, Y, Z... {} в этих X, Y, `Z`, они могут быть интерфейсом, классом или даже псевдонимом типа?

LeoPkm2-1 13.08.2024 06:13

@jcalz Ваше объяснение очень очевидно. Прежде чем вы напишете ответ, мне интересно следующее: когда класс расширяется class A {}; class B extend A {}, машинописный текст будет делать 2 вещи: первое, что нужно сделать, это расширить тип интерфейса экземпляра A с помощью типа интерфейса B, например interface B extend A {}. Второе, что он делает, это работает наследование (наследование [[Prototype]]), которое связано со значениями конструктора и временем выполнения (действие javascript). это правда?

LeoPkm2-1 13.08.2024 06:37

@jcalz Могу ли я заявить, что: «В машинописном скрипте наследованием считается только расширение класса, расширение интерфейса — это просто структура типа слияния для создания нового контракта (типа интерфейса)».

LeoPkm2-1 13.08.2024 06:40

Ваше резюме во многом верно, но я бы не стал использовать такую ​​терминологию. Я бы сказал, что interface B extends A {} — это тоже «наследование», но это наследование типов, и с множественным наследованием проблем нет, потому что нет необходимости беспокоиться о реализации. Множественное наследование реализаций потенциально может сбить с толку, поскольку тогда что-то должно выбирать, от какой реализации наследовать. Но когда речь идет только о типах, вы можете легко наследовать оба типа. Итак, могу ли я написать ответ сейчас? Мы на одной волне? Или я еще что-то упускаю.

jcalz 13.08.2024 14:59

@jcalz три резюме верны?

LeoPkm2-1 13.08.2024 15:15

@jcalz Спасибо за объяснение, оно действительно потрясающее. Не могли бы вы написать ответ?

LeoPkm2-1 13.08.2024 15:18

Я сделаю это скоро.

jcalz 13.08.2024 15:21
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
17
94
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

В целом использование наследования часто считается плохой практикой. Есть несколько хороших практик, которые вам следует запомнить:

  • Принцип разделения интерфейса (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:

  1. Да, реализация нескольких интерфейсов является обычным явлением. Вот пример в самом коде TypeScript.

  2. Да, то же самое, что и 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» — а, нет? Почему ты это пишешь?

Bergi 12.08.2024 15:15

Когда вы реализуете язык программирования, вы как разработчик можете разрешить или запретить множественное наследование. В этом случае они предпочитают его не поддерживать. Классы JS имеют единое наследование, но нужно помнить, что TypeScript был реализован задолго до того, как в JavaScript появились классы. Запрет множественного наследования всегда является наиболее вероятным случаем, поскольку существуют документированные проблемы с противоположным сайтом.

Remo H. Jansen 12.08.2024 15:22

TypeScript был основан на предложениях классов ES6 и до них не был реализован. В JS всегда было единое наследование, и он никогда не предполагал множественное наследование, поэтому дизайнеры TypeScript были в значительной степени ограничены этим; это было не их решение.

Bergi 12.08.2024 15:25

Честно говоря, я перешел в комитет ECMAScript. Я пытался подчеркнуть, что наличие множественного или единственного наследования не имеет никакой «причины», кроме выбора дизайнеров.

Remo H. Jansen 12.08.2024 15:28

Если бы 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 выглядят одинаково, они находятся в совершенно разных синтаксических контекстах и ​​ведут себя по-разному. Первое является незаконным множественным наследованием классов, а второе — допустимым множественным наследованием интерфейсов.

Детская площадка, ссылка на код

Другие вопросы по теме