← Курс/Интерфейсы#144 из 257+30 XP

Интерфейсы в TypeScript

Ключевое слово interface

Интерфейс описывает **контракт** — структуру объекта или класса:

interface User {
  id: number
  name: string
  email: string
  age?: number       // опциональное поле
  readonly createdAt: string  // только чтение
}

const user: User = {
  id: 1,
  name: 'Алексей',
  email: 'alex@mail.ru',
  createdAt: '2024-01-01',
}

extends — наследование интерфейсов

Интерфейс может расширять другой, добавляя новые поля:

interface Animal {
  name: string
  age: number
}

interface Dog extends Animal {
  breed: string
  bark(): void
}

interface GuideDog extends Dog {
  owner: string
  isWorking: boolean
}

const buddy: GuideDog = {
  name: 'Buddy',
  age: 3,
  breed: 'Labrador',
  bark() { console.log('Woof!') },
  owner: 'Алексей',
  isWorking: true,
}

implements — классы и интерфейсы

Класс может **реализовывать** интерфейс — обязан иметь все его методы и свойства:

interface Printable {
  print(): void
  toString(): string
}

class Invoice implements Printable {
  constructor(private amount: number, private client: string) {}

  print() {
    console.log(`Invoice for ${this.client}: ${this.amount}₽`)
  }

  toString() {
    return `Invoice(${this.client}, ${this.amount})`
  }
}

Index signatures

Позволяют описать объект с динамическими ключами:

interface StringMap {
  [key: string]: string  // любой строковый ключ → строковое значение
}

interface NumberMap {
  [key: string]: number
}

const translations: StringMap = {
  hello: 'привет',
  world: 'мир',
  // можно добавлять любые ключи
}

interface vs type — когда что использовать

| Возможность | interface | type |

|---|---|---|

| Объектный тип | ✓ | ✓ |

| extends / implements | ✓ | с & (intersection) |

| Declaration merging | ✓ | ✗ |

| Union типы | ✗ | ✓ |

| Mapped types | ✗ | ✓ |

**Declaration merging** — можно дополнить интерфейс в другом месте кода (используется в библиотеках):

// В одном файле:
interface Request {
  url: string
}

// В другом файле (расширяем):
interface Request {
  user?: User  // добавляем поле
}

// Итоговый Request = { url: string; user?: User }

**Правило выбора**: interface для объектов которые могут наследоваться или реализовываться классами; type для union, intersection, примитивных псевдонимов и сложных mapped types.

Примеры

Паттерн Repository — InMemoryUserRepo и InMemoryProductRepo реализуют единый "интерфейс" с методами getById, getAll, save, delete

// TypeScript позволяет явно объявить interface Repository<T>.
// В JavaScript симулируем контракт через документацию и runtime-проверки.
//
// interface Repository<T> {
//   getById(id: number): T | undefined
//   getAll(): T[]
//   save(item: T): T
//   delete(id: number): boolean
// }

class InMemoryUserRepo {
  #store = new Map()
  #nextId = 1

  save(user) {
    if (!user || typeof user.name !== 'string') {
      throw new TypeError('User должен иметь поле name: string')
    }
    if (user.id) {
      // update
      this.#store.set(user.id, { ...user })
      return this.#store.get(user.id)
    }
    // create
    const newUser = { ...user, id: this.#nextId++ }
    this.#store.set(newUser.id, newUser)
    return newUser
  }

  getById(id) {
    return this.#store.get(id)
  }

  getAll() {
    return Array.from(this.#store.values())
  }

  delete(id) {
    return this.#store.delete(id)
  }

  // Дополнительный метод (сверх интерфейса)
  findByEmail(email) {
    return this.getAll().find(u => u.email === email)
  }
}

class InMemoryProductRepo {
  #store = new Map()
  #nextId = 1

  save(product) {
    if (!product || typeof product.name !== 'string' || typeof product.price !== 'number') {
      throw new TypeError('Product должен иметь name: string и price: number')
    }
    if (product.id) {
      this.#store.set(product.id, { ...product })
      return this.#store.get(product.id)
    }
    const newProduct = { ...product, id: this.#nextId++ }
    this.#store.set(newProduct.id, newProduct)
    return newProduct
  }

  getById(id) {
    return this.#store.get(id)
  }

  getAll() {
    return Array.from(this.#store.values())
  }

  delete(id) {
    return this.#store.delete(id)
  }

  // Дополнительный метод
  findByMaxPrice(maxPrice) {
    return this.getAll().filter(p => p.price <= maxPrice)
  }
}

// Функция работающая с ЛЮБЫМ репозиторием через общий "интерфейс"
// В TypeScript: function printAll<T>(repo: Repository<T>): void
function printAll(repo, label) {
  const items = repo.getAll()
  console.log(`${label} (${items.length} шт.):`)
  items.forEach(item => console.log('  ', JSON.stringify(item)))
}

// --- Демонстрация ---
const users = new InMemoryUserRepo()
const products = new InMemoryProductRepo()

console.log('=== Users Repository ===')
const alice = users.save({ name: 'Алиса', email: 'alice@mail.ru' })
const bob = users.save({ name: 'Боб', email: 'bob@mail.ru' })
console.log('Создано:', alice, bob)
users.save({ ...alice, name: 'Алиса Обновлённая' })

printAll(users, 'Пользователи')
console.log('findByEmail:', users.findByEmail('bob@mail.ru'))
users.delete(bob.id)
printAll(users, 'После удаления Боба')

console.log('\n=== Products Repository ===')
products.save({ name: 'Ноутбук', price: 75000 })
products.save({ name: 'Мышь', price: 1500 })
products.save({ name: 'Клавиатура', price: 3000 })
printAll(products, 'Продукты')
console.log('До 5000₽:', products.findByMaxPrice(5000))