Интерфейсы
Interface — способ описания структуры объекта в TypeScript. Поддерживает наследование, реализацию в классах и declaration merging.
Зачем нужно
- Описание контрактов: какие свойства и методы должен иметь объект
- Наследование через
extends— расширение существующих типов - Реализация в классах через
implements— гарантия контракта - Declaration merging — расширение типов из сторонних библиотек
Где используется
- Описание структуры объектов, пропсов компонентов, API-ответов
- Контракты для классов (implements)
- Расширение типов сторонних библиотек
- Описание формы данных в приложении
Предпосылки
- Примитивные типы — базовые типы
- Type alias — альтернатива interface
Базовый синтаксис
interface User {
id: number;
name: string;
email: string;
}
// Объект должен соответствовать структуре
const user: User = {
id: 1,
name: "Alice",
email: "alice@example.com",
};
// Ошибки:
const bad: User = {
id: 1,
name: "Alice",
// Ошибка! Свойство 'email' отсутствует
};
const bad2: User = {
id: 1,
name: "Alice",
email: "a@b.com",
role: "admin", // Ошибка! 'role' не существует в типе User
};
Свойства интерфейса
interface Product {
// Обязательное свойство
id: number;
// Опциональное свойство
description?: string; // string | undefined
// Только для чтения
readonly createdAt: Date;
// Метод (два синтаксиса — равнозначны)
getPrice: number;
getPrice: => number;
}
const product: Product = {
id: 1,
createdAt: new Date,
getPrice {
return 99.99;
},
};
product.id = 2; // OK
product.createdAt = new Date(); // Ошибка! readonly
Наследование (extends)
interface Animal {
name: string;
age: number;
}
interface Pet extends Animal {
owner: string;
}
// Pet имеет: name, age, owner
const cat: Pet = {
name: "Whiskers",
age: 3,
owner: "Alice",
};
// Множественное наследование
interface HasId {
id: number;
}
interface HasTimestamps {
createdAt: Date;
updatedAt: Date;
}
interface Entity extends HasId, HasTimestamps {
name: string;
}
// Entity: { id, createdAt, updatedAt, name }
Переопределение свойств при наследовании
interface Base {
value: string | number;
id: number;
}
interface Derived extends Base {
value: string; // OK — сужение типа (string — подтип string | number)
// id: string; // Ошибка! string не совместим с number
}
Реализация в классах (implements)
interface Serializable {
serialize: string;
deserialize(data: string): void;
}
interface Loggable {
log(message: string): void;
}
// Класс реализует интерфейс(ы)
class User implements Serializable, Loggable {
constructor(public name: string, public age: number) {}
serialize: string {
return JSON.stringify({ name: this.name, age: this.age });
}
deserialize(data: string): void {
const parsed = JSON.parse(data);
this.name = parsed.name;
this.age = parsed.age;
}
log(message: string): void {
console.log(`[User ${this.name}] ${message}`);
}
}
Declaration Merging
Несколько объявлений interface с одним именем объединяются:
interface User {
id: number;
name: string;
}
// Позже — в другом файле или месте
interface User {
email: string;
age?: number;
}
// Результат — объединение:
// interface User {
// id: number;
// name: string;
// email: string;
// age?: number;
// }
const user: User = {
id: 1,
name: "Alice",
email: "alice@example.com",
};
Расширение сторонних библиотек
// Расширяем типы Express
declare module "express" {
interface Request {
userId?: string;
sessionData?: Record<string, unknown>;
}
}
// Теперь req.userId доступен без ошибок
app.use((req, res, next) => {
req.userId = "123"; // OK
next;
});
// Расширяем Window
interface Window {
__APP_CONFIG__: {
apiUrl: string;
debug: boolean;
};
}
window.__APP_CONFIG__.apiUrl; // OK
Index Signatures
Описание объектов с динамическими ключами:
// Строковый индекс
interface Dictionary {
[key: string]: string;
}
const dict: Dictionary = {
hello: "привет",
world: "мир",
};
// Числовой индекс
interface StringArray {
[index: number]: string;
}
const arr: StringArray = ["a", "b", "c"];
// Комбинация фиксированных и динамических свойств
interface Config {
name: string; // Фиксированное свойство
version: number; // Фиксированное свойство
[key: string]: string | number; // Все остальные ключи
// Фиксированные свойства должны быть совместимы с index signature
}
// Record-подобный паттерн
interface UserMap {
[userId: string]: {
name: string;
age: number;
};
}
Callable и Constructable Interfaces
// Callable — описание функции
interface Formatter {
(input: string): string;
}
const upper: Formatter = (s) => s.toUpperCase();
// Callable с свойствами
interface Counter {
: number; // Вызов
reset: void; // Метод
count: number; // Свойство
}
function createCounter: Counter {
const fn = function {
return ++fn.count;
} as Counter;
fn.count = 0;
fn.reset() = () => { fn.count = 0; };
return fn;
}
// Constructable — описание конструктора
interface UserConstructor {
new (name: string, age: number): User;
}
Generics в интерфейсах
interface Repository<T> {
getById(id: number): T | null;
getAll: T;
create(item: Omit<T, "id">): T;
update(id: number, item: Partial<T>): T;
delete(id: number): boolean;
}
interface User {
id: number;
name: string;
email: string;
}
class UserRepository implements Repository<User> {
private users: User = ;
getById(id: number): User | null {
return this.users.find((u) => u.id === id) ?? null;
}
getAll: User {
return [...this.users];
}
create(item: Omit<User, "id">): User {
const user = { ...item, id: Date.now() };
this.users.push(user);
return user;
}
update(id: number, item: Partial<User>): User {
const index = this.users.findIndex((u) => u.id === id);
this.users[index] = { ...this.users[index], ...item };
return this.users[index];
}
delete(id: number): boolean {
const index = this.users.findIndex((u) => u.id === id);
if (index === -1) return false;
this.users.splice(index, 1);
return true;
}
}
Частые ошибки
- Excess property checking — лишние свойства при прямом присвоении
interface User {
name: string;
}
// Ошибка — лишнее свойство при прямом присвоении
const user: User = { name: "Alice", age: 30 };
// Error: 'age' does not exist in type 'User'
// НО через промежуточную переменную — OK
const data = { name: "Alice", age: 30 };
const user: User = data; // OK! Структурная типизация
- Конфликт при declaration merging — несовместимые типы одного свойства
- Index signature vs конкретные свойства — конкретные свойства должны быть подтипом index signature
- implements не добавляет типы — класс должен реализовать всё сам
interface HasName {
name: string;
}
class User implements HasName {
// name НЕ добавляется автоматически — нужно объявить
name: string; // Обязательно!
constructor(name: string) {
this.name = name;
}
}
- Мутация readonly свойств через алиас — readonly только на уровне TypeScript
Практика
- Создайте интерфейс
ApiResponse<T>с полямиdata,status,message - Расширите интерфейс через
extends:BaseEntity→User→AdminUser - Добавьте declaration merging для расширения
Window - Реализуйте интерфейс
Repository<T>в классе - Создайте callable interface с дополнительными свойствами
Связанные темы
- Type alias — альтернативный способ описания типов
- Type vs Interface — когда что использовать
- Классы — реализация интерфейсов
- Generics — обобщённые интерфейсы