Що робить implements у TypeScript?
implements у TypeScript - це контракт часу компіляції: клас повинен відповідати кожному члену інтерфейсу, інакше компілятор зупиниться.
Теорія
TL;DR
- Уяви список перевірки від санітарної інспекції. Інтерфейс - це вимоги, клас - кухня, яку перевіряють.
implementsперевіряє форму, а не поведінку. Нічого з інтерфейсу не копіюється в клас.- Використовуй, коли потрібна структурна гарантія без успадкування.
class Foo implements A, Bповинен задовольнити обидва інтерфейси.- Жодних витрат у рантаймі: інтерфейси зникають при компіляції. Node бачить звичайний JS.
Швидкий приклад
interface Shape {
area(): number;
}
class Circle implements Shape {
constructor(private radius: number) {}
area() {
return Math.PI * this.radius ** 2; // Вимагається Shape
}
}Прибери area() - TypeScript повідомить: Property 'area' is missing in type 'Circle' but required in type 'Shape'. У скомпільованому JS інтерфейсу немає жодного сліду.
implements vs extends
extends копіює методи батьківського класу через ланцюжок прототипів. implements лише перевіряє форму, жоден код не передається. Саме тому клас може реалізувати кілька інтерфейсів (implements A, B), але розширити - тільки один батьківський клас.
Обирай implements, коли потрібен структурний контракт без прив'язки до ланцюжка прототипів. Обирай extends, коли потрібен спільний код.
Коли використовувати
- API плагінів:
class MyPlugin implements Pluginзмушує користувачів відповідати контракту - NestJS:
class AuthGuard implements CanActivate - Тестування: гарантує, що мок-класи мають усі методи реального класу
- Кілька типів поведінки:
class Duck implements Flyable, Swimmable - Redux:
class IncrementAction implements Action
Як це обробляє компілятор
Під час tsc TypeScript обходить AST класу та порівнює кожен член інтерфейсу з визначенням класу. Відсутній член або невідповідна сигнатура - компілятор видає помилку. У JS-вивід нічого з цього не потрапляє. У рантаймі V8 бачить звичайний синтаксис класів без жодних метаданих про інтерфейси.
Типові помилки
Очікування, що implements скопіює реалізацію
interface HasGreet {
greet(): string;
}
class Bot implements HasGreet {
// Помилка: Property 'greet' is missing
}Бачив це на код-рев'ю: хтось додає implements і чекає, що метод з'явиться сам. Не з'явиться. Пишемо самостійно.
Використання implements з конкретним класом замість extends
class Animal {
move() {}
}
class Dog implements Animal { // Помилка: Property 'move' is missing
bark() {}
}Щоб успадкувати move(), використовуй extends Animal. З implements кожен член потрібно визначати вручну, навіть якщо цільовий тип - клас, а не інтерфейс.
Невідповідність модифікаторів доступу
interface User {
getName(): string;
}
class Admin implements User {
private getName() { return "admin"; } // Помилка: private порушує публічний контракт
}Інтерфейси описують публічний API. Метод зі специфікатором private не задовольнить вимогу інтерфейсу. Прибери private або явно постав public.
Забутий ? у необов'язкових членах інтерфейсу
interface Swimmable {
swim?(): void; // Необов'язково в інтерфейсі
}
class Duck implements Swimmable {
swim() { console.log("Paddle!"); } // Нормально - optional означає, що можна і пропустити
}Прибери ? з інтерфейсу - і swim() стає обов'язковим для кожного класу, який реалізує Swimmable. Легко пропустити під час рефакторингу.
Де зустрічається в реальному коді
- NestJS:
class AuthGuard implements CanActivate,class Logger implements NestInterceptor - Express: класи middleware, що відповідають сигнатурі middleware-функції
- React: мок-реалізації у Jest, які відповідають реальному API компонента
- Vue 3:
class MyPlugin implements Plugin - Redux:
class IncrementAction implements Action
Питання на співбесіді
Q: Яка різниця між implements і extends?
A: extends копіює методи через ланцюжок прототипів і працює тільки з класами. implements перевіряє форму і працює тільки з інтерфейсами. Клас може розширювати один батьківський клас, але реалізовувати - кілька інтерфейсів.
Q: Чи може implements перевіряти приватні члени?
A: Ні. Інтерфейси описують лише публічну форму. Метод зі специфікатором private не задовольнить вимогу інтерфейсу.
Q: Що станеться, якщо два реалізованих інтерфейси мають члени з однаковою назвою, але різними типами повернення?
A: TypeScript вимагає задоволення обох контрактів. Конфліктні типи повернення дадуть помилку компіляції. Автоматичного вирішення немає.
Q: Як implements взаємодіє зі злиттям декларацій (declaration merging)?
A: Якщо інтерфейс оголошено двічі, TypeScript об'єднує їх в одну структуру. Клас повинен задовольнити обидва оголошення. Оголоси Foo з методом a(), потім знову з b() - і implements Foo вимагатиме обох.
Приклади
Базовий: контракт форми
interface Shape {
area(): number;
perimeter(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area() {
return this.width * this.height;
}
perimeter() {
return 2 * (this.width + this.height);
}
}
const rect = new Rectangle(4, 5);
console.log(rect.area()); // 20
console.log(rect.perimeter()); // 18Обидва методи вимагає інтерфейс. Прибери один - tsc зупинить збірку з точним повідомленням про відсутній член.
Середній рівень: кілька інтерфейсів
interface Flyable {
fly(): void;
}
interface Loggable {
log(message: string): void;
}
class Drone implements Flyable, Loggable {
fly() {
console.log("Drone is airborne");
}
log(message: string) {
console.log(`[Drone] ${message}`);
}
}
const drone = new Drone();
drone.fly(); // "Drone is airborne"
drone.log("Battery at 80%"); // "[Drone] Battery at 80%"Drone задовольняє обидва контракти. Ланцюжок прототипів залишається плоским. Додай третій інтерфейс - список зростає без жодної ієрархії успадкування. Ось у чому сенс: композиція без успадкування.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.