interface Child extends Parent создаёт новый интерфейс со всеми членами Parent плюс собственными. Значения типа потомка можно использовать везде, где ожидается родитель, потому что у них есть все его свойства.
Это же ключевое слово встречается в TypeScript в четырёх местах, со связанными, но разными значениями:
| Где | Пример | Значение |
|---|---|---|
| Интерфейс | interface Dog extends Animal {} | унаследовать члены родителя |
| Класс | class Dog extends Animal {} | унаследовать поля и реализации методов (JavaScript времени выполнения) |
| Параметр дженерика | <T extends Animal> | ограничение: аргумент типа должен присваиваться в Animal |
| Условный тип | T extends string ? A : B | проверка: присваивается ли T в string? |
Расширение нескольких интерфейсов
Перечислите несколько родителей после extends через запятую. Результат содержит члены их всех:
interface Identified {
id: number;
}
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface Post extends Identified, Timestamped {
title: string;
}
// Post requires id, createdAt, updatedAt and title
Если два родителя объявляют одно и то же свойство с неидентичными типами, потомок даёт ошибку TS2320 (Interface 'Post' cannot simultaneously extend types 'A' and 'B'), если только он не переобъявит это свойство с типом, который присваивается обоим.
Расширение псевдонима типа
У псевдонима type нет конструкции extends. Надстраивать его можно пересечением &, которое объединяет члены обеих сторон. Интерфейс тоже может напрямую расширять псевдоним объектного типа, а псевдоним типа может пересекаться с интерфейсом: эти два вида свободно смешиваются.
Оба пути ведут себя одинаково, пока свойства не конфликтуют: extends сообщает о конфликтующем свойстве в объявлении, а & молча превращает его в never. Сравнение есть на странице interface или type, а подробности про & на странице о типах пересечения.
Переопределение типов свойств
Интерфейс-потомок может переобъявить свойство родителя, но только с типом, который присваивается типу родителя. Сузить string до литерала или убрать null из объединения можно. Изменить тип на несвязанный нельзя:
index.ts(13,11): error TS2430: Interface 'Label' incorrectly extends interface 'Shape'.
Types of property 'kind' are incompatible.
Type 'number' is not assignable to type 'string'.
Правило существует для того, чтобы Label можно было использовать везде, где ожидается Shape. Если бы kind мог быть числом, код, написанный для Shape и вызывающий shape.kind.toUpperCase(), сломался бы.
Замена свойств через Omit
Когда унаследованному свойству действительно нужен другой тип, сначала удалите его из родителя через Omit, затем объявите новое. Частый случай это преобразование данных API, где идентификаторы и даты приходят строками:
Omit<ApiUser, "id" | "createdAt"> это { name: string }, поэтому новые id и createdAt ни с чем не конфликтуют. Pick, Partial и другие служебные типы расширяются так же.
extends в классах
class Dog extends Animal это наследование классов JavaScript, и в отличие от extends в интерфейсах оно существует во время выполнения: Dog получает конструктор, поля и реализации методов Animal, а new Dog(...) instanceof Animal равно true. TypeScript добавляет сверху проверки типов, например ключевое слово override:
Класс может расширять (extends) только один класс, но реализовывать (implements) любое число интерфейсов. implements ничего не копирует: он только проверяет, что класс объявляет то, чего требует интерфейс. super, защищённые члены и override описаны в разделе наследование.
extends в дженериках и условных типах
Внутри угловых скобок extends это ограничение. <T extends { length: number }> принимает любой аргумент типа с числовым length и позволяет телу функции использовать .length. В условном типе T extends U ? X : Y спрашивает, присваивается ли T в U.
В обоих местах extends означает «присваивается в», то же отношение, которое проверяет форма для интерфейсов. Ограничения подробно разобраны в разделе ограничения дженериков, а проверки на уровне типов в разделе условные типы.
Часто задаваемые вопросы
Как расширить интерфейс в TypeScript?
Напишите interface Child extends Parent { ... }. Потомок получает все свойства родителя плюс те, что объявляет сам. Чтобы расширить несколько интерфейсов сразу, перечислите их через запятую: interface C extends A, B { ... }.
Как расширить псевдоним типа в TypeScript?
Псевдоним типа не может использовать extends; вместо этого объединяйте типы пересечением: type Admin = User & { permissions: string[] }. Интерфейс тоже может напрямую расширять псевдоним объектного типа: interface Admin extends User { permissions: string[] }.
Можно ли переопределить тип свойства при расширении интерфейса?
Только совместимым, более узким типом: kind: "circle" может заменить kind: string. Другой тип, например number для свойства string, это ошибка TS2430 (Interface 'X' incorrectly extends interface 'Y'). Чтобы заменить свойство несвязанным типом, сначала удалите его: interface User extends Omit<ApiUser, "id"> { id: number }.
Чем extends отличается от implements?
extends наследует: интерфейс получает члены родителя, а класс получает поля и реализации методов родительского класса. implements только проверяет: класс, реализующий интерфейс, должен сам объявить каждый член и ничего не получает от интерфейса.
Что значит extends в дженерике вроде <T extends string>?
Это ограничение, а не наследование. T extends string означает, что аргумент типа должен присваиваться в string, поэтому внутри функции можно использовать методы строк на T. В условном типе T extends U ? X : Y это проверка: если T присваивается в U, результат X, иначе Y.