TypeScriptMiddleTechnical
Что такое abstract-классы в TypeScript и чем они отличаются от интерфейсов?
abstract-класс существует в JavaScript после компиляции: может иметь поля, конструктор и реализацию методов. interface — только compile-time контракт, исчезает после сборки. Используйте abstract class, когда нужна общая логика; interface — для описания формы объекта.
Абстрактные классы
Абстрактный класс объявляется ключевым словом abstract и не может быть инстанциирован напрямую. Он существует в скомпилированном JavaScript как обычный класс и может содержать:
- конкретные методы с реализацией;
- конструктор с инициализацией полей;
- абстрактные методы без тела — наследники обязаны их реализовать;
- защищённые (
protected) и приватные поля.
abstract class Repository<T> {
protected readonly tableName: string;
constructor(tableName: string) {
this.tableName = tableName;
}
// Общая реализация — наследуется как есть
protected now(): Date {
return new Date();
}
// Контракт — каждый наследник реализует по-своему
abstract findById(id: string): Promise<T | null>;
abstract save(entity: T): Promise<void>;
}
interface User {
id: string;
email: string;
}
class UserRepository extends Repository<User> {
constructor() {
super('users');
}
async findById(id: string): Promise<User | null> {
console.log(`SELECT * FROM ${this.tableName} WHERE id = '${id}'`);
// Симуляция запроса
return id === '1' ? { id: '1', email: 'test@example.com' } : null;
}
async save(user: User): Promise<void> {
const ts = this.now().toISOString();
console.log(`INSERT INTO ${this.tableName} VALUES ('${user.id}', '${user.email}', '${ts}')`);
}
}
async function main() {
const repo = new UserRepository();
const user = await repo.findById('1');
console.log(user); // { id: '1', email: 'test@example.com' }
await repo.save({ id: '2', email: 'new@example.com' });
}
main();
Интерфейсы
Интерфейс — это исключительно compile-time конструкция. После компиляции в JavaScript он полностью исчезает. Интерфейсы описывают форму объекта и поддерживают:
- множественную реализацию (
implements A, B, C); - расширение нескольких интерфейсов через
extends; - слияние деклараций (declaration merging) — два блока с одним именем объединяются.
interface Serializable {
toJSON(): unknown;
}
interface Auditable {
createdAt: Date;
updatedAt: Date;
}
// Класс может реализовывать несколько интерфейсов
class Order implements Serializable, Auditable {
createdAt = new Date();
updatedAt = new Date();
constructor(public id: string, public total: number) {}
toJSON() {
return { id: this.id, total: this.total };
}
}
// Интерфейс работает и для структурной типизации plain-объектов
interface Config {
host: string;
port: number;
tls?: boolean;
}
function connect(cfg: Config) {
console.log(`Connecting to ${cfg.host}:${cfg.port}`);
}
connect({ host: 'localhost', port: 5432 }); // OK, нет class вовсе
Ключевые различия
- Runtime-существование: abstract class → присутствует в JS; interface → стирается при компиляции.
- Реализация: abstract class может содержать готовый код; interface — только сигнатуры.
- Наследование: класс может
extendsтолько один abstract class, ноimplementsлюбое число интерфейсов. - Состояние: abstract class хранит поля и инициализирует их в конструкторе; interface не может.
- Declaration merging: интерфейсы допускают слияние деклараций в разных файлах; классы — нет.
- Совместимость с plain-объектами: interface подходит для любого объекта нужной формы; abstract class требует наследования.
Когда что выбирать
Выбирайте abstract class, когда:
- несколько наследников разделяют общую логику (утилитные методы, инициализация);
- нужно гарантировать порядок инициализации через конструктор;
- вы строите иерархию с шаблонным методом (Template Method pattern).
Выбирайте interface, когда:
- описываете контракт для plain-объектов, DTO, конфигов;
- нужна множественная реализация контракта;
- хотите использовать structural typing без привязки к иерархии классов.
Подводные камни
- instanceof с интерфейсами не работает:
obj instanceof MyInterface— ошибка компиляции, потому что интерфейс стёрт. Для runtime-проверки нужен abstract class или type guard. - Абстрактный класс нельзя создать напрямую:
new Repository('users')выдаст ошибку TypeScript. Это ожидаемо, но часто удивляет новичков, пришедших из JavaScript. - Слияние деклараций интерфейсов может быть неожиданным: если в двух файлах объявлен
interface Window, TypeScript объединит их. Это полезно для расширения глобальных типов, но ломает логику при случайном совпадении имён. - abstract class в React: нельзя использовать как тип пропсов напрямую без дополнительной обвязки; интерфейсы подходят лучше для описания пропсов компонентов.
- Приватные поля abstract class не наследуются: поле с модификатором
privateнедоступно в подклассе; нужноprotected. - Множественное наследование реализации невозможно: если два abstract class содержат метод с одинаковым именем, нельзя унаследовать оба — TypeScript разрешает только один
extends. - interface не проверяет лишние поля при присваивании через переменную:
const x: Config = { host: 'a', port: 1, extra: true }— ошибка только при литеральном присваивании, но не через промежуточную переменную (excess property check срабатывает не всегда). - Абстрактные методы без реализации в наследнике: если наследник не реализует все абстрактные методы, TypeScript выдаст ошибку, однако при использовании
// @ts-ignoreили генерации кода это может проскочить и упасть в runtime.
Common mistakes
- Смешивать «abstract-классы и интерфейсы» с похожим механизмом без критерия выбора.
- Игнорировать риск: неверно оценить границы применения темы «abstract-классы и интерфейсы» и получить хрупкое решение.
- Показывать только синтаксис и не объяснять поведение в runtime или сборке.
What the interviewer is testing
- Объясняет runtime-наследование с общей реализацией против compile-time контракта формы.
- Показывает на примере, как работает: abstract class существует в JavaScript после компиляции и может содержать поля, constructor и методы, а interface исчезает и только проверяет структуру.
- Называет production-нюанс и граничный случай для темы «abstract-классы и интерфейсы».