Menu

Наследование в TypeScript: extends, super и override

Наследование классов в TypeScript: extends и super, переопределение методов с совместимыми типами, ключевое слово override и noImplicitOverride, защищённые члены, переобъявление полей через declare и когда implements подходит лучше.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Класс TypeScript наследует от другого через extends. Подкласс получает все поля и методы родителя, вызывает родительский конструктор через super(...) и может переопределять методы, а TypeScript проверяет, что каждое переопределение остаётся совместимым с типами родителя.

Это наследование классов JavaScript (цепочка прототипов, которая за ним стоит, описана в разделе наследование в JavaScript). TypeScript добавляет проверки типов: Dog можно присвоить везде, где ожидается Animal, а компилятор проверяет переопределение и вызов super.

Переопределение методов

Метод подкласса с тем же именем заменяет метод родителя. Его тип должен присваиваться типу родителя: такие же или совместимые параметры, такой же или более узкий тип возврата. Иначе любой код, держащий Animal, мог бы вызвать метод с аргументами, которые подкласс не умеет обрабатывать.

Изменение параметра на value: string было бы ошибкой TS2416: Property 'format' in type 'CurrencyFormatter' is not assignable to the same property in base type 'Formatter'. Цикл показывает почему: он вызывает format с числом на каждом элементе, не зная, какой перед ним подкласс.

Есть одна дыра. Параметры методов сравниваются бивариантно, поэтому переопределение, которое принимает более узкий тип, например format(value: 1 | 2), компилируется, хотя цикл по-прежнему может передать ему 1234567. Типы возврата проверяются строго: возврат number вместо string это TS2416.

Ключевое слово override и noImplicitOverride

override говорит «этот член заменяет член базового класса», и компилятор требует, чтобы это было правдой. Если в базовом классе такого члена нет (опечатка или метод, который кто-то переименовал в родителе), это ошибка:

index.ts(9,14): error TS4117: This member cannot have an 'override' modifier because it is not declared in the base class 'Animal'. Did you mean 'speak'?

Без override эта опечатка компилируется, и speak() молча возвращает "...". Ключевое слово помогает только там, где оно написано, поэтому включите noImplicitOverride в tsconfig.json. Тогда каждый метод, переопределяющий член базового класса, обязан это указать, а обычный speak() в подклассе будет ошибкой TS4114 (This member must have an 'override' modifier because it overrides a member in the base class 'Animal').

{
    "compilerOptions": {
        "strict": true,
        "noImplicitOverride": true
    }
}

noImplicitOverride не входит в strict; его нужно добавить самостоятельно. override стирается из скомпилированного JavaScript.

Защищённые члены в подклассах

Члены private родителя в подклассе не видны. Члены protected видны, и именно для этого protected существует: это API, который родительский класс предлагает потомкам.

Полные правила, включая то, почему ни private, ни protected не соблюдаются во время выполнения, см. в разделе модификаторы доступа.

Переобъявление поля с более узким типом

Подкласс часто знает, что поле хранит нечто более конкретное, чем говорит родитель. Переобъявление его как pet: Dog при современных полях классов имеет эффект во время выполнения: поле подкласса инициализируется значением undefined после возврата из super(), стирая то, что сохранил родительский конструктор. Если конструктор подкласса не присвоит его снова, TypeScript сообщает об этом как TS2612 (Property 'pet' will overwrite the base property in 'Home'). Используйте declare, чтобы изменить только тип:

declare ничего не генерирует, поэтому значение, заданное конструктором Home, сохраняется. Более узкий тип становится правдой благодаря конструктору, который принимает только Dog.

implements и extends

Эти два ключевых слова отвечают на разные вопросы, и класс может использовать оба.

extendsimplements
Работает содним классомлюбым числом интерфейсов (или псевдонимов объектных типов)
Приносит кодда: поля, методы, конструкторнет, только проверяет структуру
Существует во время выполненияда, цепочка прототиповнет, стирается
instanceof работаетданет
Типичное применениеразделить настоящее поведениепообещать структуру остальному коду

Класс может расширять только один класс (TS1174: Classes can only extend a single class). Чтобы объединить поведение из нескольких мест, реализуйте несколько интерфейсов и делитесь кодом через композицию (поле, хранящее вспомогательный объект) или миксины.

Миксины

Миксин это функция, которая принимает класс и возвращает его подкласс. Она даёт форму множественного наследования, проходящую проверку типов:

any[] в Constructor обязателен: TypeScript принимает основу миксина, только если её конструктор принимает единственный rest-параметр типа any[]. Миксины работают, но усложняют чтение типов и трассировок стека, поэтому сначала пробуйте композицию.

Частые ошибки

  • Вызов переопределяемого метода из базового конструктора. Базовый конструктор выполняется до инициализации полей подкласса, поэтому переопределение, читающее this.items, видит undefined, а вызов вроде this.items.push(x) выбрасывает TypeError во время выполнения. TypeScript этого не ловит.
  • Забытый или поздний вызов super(...). Ошибки TS2377 и TS17009; родитель должен построить объект до того, как подкласс обратится к this.
  • Переобъявление поля родителя без declare. Поле подкласса сбрасывает его в undefined.
  • Глубокие иерархии. Каждый уровень связывает потомка с внутренностями родителя. Обычно двух уровней достаточно; дальше предпочитайте композицию или интерфейсы.

Часто задаваемые вопросы

Как работает наследование в TypeScript?

Класс наследует от другого через extends: class Dog extends Animal. Подкласс получает поля и методы родителя, вызывает родительский конструктор через super(...) и может переопределять методы. TypeScript проверяет, что переопределения остаются совместимыми по типам с родителем.

Что делает ключевое слово override в TypeScript?

override помечает метод или свойство, которое заменяет член базового класса. Если в базовом классе нет члена с таким именем, компилятор сообщает TS4117, и это ловит опечатки и члены, переименованные в родителе. При включённом noImplicitOverride каждое переопределение должно нести это ключевое слово (иначе TS4114).

Как вызвать метод родительского класса в TypeScript?

Используйте super.methodName(...) внутри метода подкласса. В конструкторе super(...) вызывает родительский конструктор и должен выполняться до любого использования this.

Поддерживает ли TypeScript множественное наследование?

Нет. Класс может расширять только один класс (TS1174: Classes can only extend a single class). Он может реализовать любое число интерфейсов, а миксины (функции, которые принимают класс и возвращают подкласс) объединяют поведение из нескольких источников.

Чем extends отличается от implements?

extends наследует настоящий код родительского класса. implements только проверяет, что класс соответствует структуре интерфейса, и ничего не добавляет во время выполнения. Класс может делать и то и другое: class Dog extends Animal implements Pet.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ