← Курс/Module Augmentation: расширение чужих типов#187 из 257+30 XP

Module Augmentation: расширение чужих типов

Что такое Module Augmentation

Module Augmentation позволяет добавлять новые типы в уже существующие модули — включая сторонние библиотеки — без изменения их исходного кода. Это мощный инструмент, когда нужно расширить типы npm-пакета или добавить собственные поля к существующим интерфейсам.

Расширение типов сторонней библиотеки

Допустим, библиотека express не знает о вашем поле user на объекте Request. Вы можете добавить его через declare module:

// types/express.d.ts
import { User } from './models/User'

declare module 'express-serve-static-core' {
  interface Request {
    user?: User
    sessionId?: string
  }
}

Теперь везде в проекте req.user будет иметь правильный тип.

Слияние деклараций интерфейсов

TypeScript поддерживает **declaration merging** — одноимённые интерфейсы сливаются:

// lib.ts — оригинальный интерфейс
interface Config {
  debug: boolean
  port: number
}

// my-module.ts — расширение в том же проекте
interface Config {
  customField: string  // добавляется к оригинальному Config
}

// Итог: Config имеет debug, port И customField
const config: Config = {
  debug: true,
  port: 3000,
  customField: 'hello',  // OK
}

Глобальная аугментация

Можно добавлять типы в глобальное пространство имён — например, в Window:

// types/global.d.ts
declare global {
  interface Window {
    analytics: {
      track(event: string, data?: Record<string, unknown>): void
    }
    __APP_VERSION__: string
  }

  // Расширение встроенного Array
  interface Array<T> {
    last(): T | undefined
  }
}

// Реализация:
Array.prototype.last = function() {
  return this[this.length - 1]
}

export {}  // Делает файл модулем, не скриптом

Аугментация namespace

Для библиотек с namespace-декларациями:

// Расширяем namespace библиотеки
declare namespace MyLib {
  interface Options {
    timeout: number
    retries: number
    // Добавляем своё поле:
    customHeaders?: Record<string, string>
  }
}

Аугментация через re-export

Паттерн для обёртки сторонней библиотеки с расширенными типами:

// my-axios.ts
import axios from 'axios'

declare module 'axios' {
  interface AxiosRequestConfig {
    _retry?: boolean
    _retryCount?: number
  }
}

export default axios

Важные правила

1. Файл с declare module должен быть **модулем** — содержать хотя бы один import или export {}.

2. Глобальные аугментации требуют declare global { }.

3. Нельзя добавлять новые **type alias** через аугментацию — только члены интерфейса или namespace.

4. Файлы .d.ts с аугментациями должны быть в include вашего tsconfig.

Когда использовать

  • Express/Fastify: добавить типизированные поля в Request/Reply
  • Jest: расширить типы матчеров через expect.extend
  • Vuex/Pinia: типизировать store в компонентах
  • Любая библиотека без достаточных типов в @types
  • Примеры

    Симуляция module augmentation: расширение объектов через Object.assign и прототипы — JS-эквивалент добавления полей к чужим типам

    // В TypeScript declare module расширяет ТИПЫ существующих модулей.
    // В JS мы можем симулировать это через прототипы и monkey-patching.
    
    // --- Симуляция: расширение "чужой" библиотеки ---
    
    // Допустим, это код сторонней библиотеки (мы не можем его менять):
    class LibraryRequest {
      constructor(path, method = 'GET') {
        this.path = path
        this.method = method
        this.headers = {}
      }
    
      getPath() { return this.path }
      getMethod() { return this.method }
    }
    
    class LibraryResponse {
      constructor(statusCode, body) {
        this.statusCode = statusCode
        this.body = body
      }
    
      send() {
        return `HTTP ${this.statusCode}: ${JSON.stringify(this.body)}`
      }
    }
    
    // --- Module Augmentation в JS: добавляем поля через прототип ---
    // TypeScript: declare module 'library' { interface Request { user?: User } }
    
    LibraryRequest.prototype.user = null
    LibraryRequest.prototype.sessionId = null
    
    LibraryRequest.prototype.setUser = function(user) {
      this.user = user
      return this
    }
    
    LibraryRequest.prototype.getUser = function() {
      return this.user
    }
    
    // --- Глобальная аугментация Array (как declare global) ---
    // TypeScript: declare global { interface Array<T> { last(): T | undefined } }
    if (!Array.prototype.last) {
      Array.prototype.last = function() {
        return this[this.length - 1]
      }
    }
    
    if (!Array.prototype.first) {
      Array.prototype.first = function() {
        return this[0]
      }
    }
    
    // --- Declaration Merging симуляция: слияние объектов ---
    function createConfig(base, extension) {
      // Аналог слияния двух interface Config { ... }
      return Object.assign({}, base, extension)
    }
    
    const baseConfig = { debug: false, port: 3000 }
    const myExtension = { customField: 'hello', timeout: 5000 }
    const mergedConfig = createConfig(baseConfig, myExtension)
    
    // --- Демонстрация ---
    
    console.log('=== Расширение LibraryRequest ===')
    const req = new LibraryRequest('/api/users', 'GET')
    req.setUser({ id: 1, name: 'Иван', role: 'admin' })
    req.sessionId = 'abc-123'
    
    console.log('path:', req.getPath())
    console.log('method:', req.getMethod())
    console.log('user:', req.getUser())
    console.log('sessionId:', req.sessionId)
    
    console.log('\n=== Middleware pipeline (как Express) ===')
    function authMiddleware(req, res, next) {
      // Имитируем JWT decode
      req.user = { id: 42, name: 'Мария', role: 'user' }
      console.log('authMiddleware: user установлен')
      next()
    }
    
    function adminMiddleware(req, res, next) {
      if (req.user?.role !== 'admin') {
        console.log('adminMiddleware: доступ запрещён для', req.user?.role)
        return
      }
      next()
    }
    
    const req2 = new LibraryRequest('/admin/users', 'GET')
    const res2 = new LibraryResponse(200, { users: [] })
    
    let nextCalled = false
    authMiddleware(req2, res2, () => {
      nextCalled = true
      adminMiddleware(req2, res2, () => {
        console.log('Доступ разрешён! Ответ:', res2.send())
      })
    })
    
    console.log('\n=== Глобальная аугментация Array ===')
    const numbers = [1, 2, 3, 4, 5]
    console.log('last():', numbers.last())    // 5
    console.log('first():', numbers.first())  // 1
    
    const words = ['TypeScript', 'JavaScript', 'Rust']
    console.log('last():', words.last())   // Rust
    console.log('first():', words.first()) // TypeScript
    
    console.log('\n=== Declaration Merging (слияние конфигов) ===')
    console.log('mergedConfig:', mergedConfig)
    // { debug: false, port: 3000, customField: 'hello', timeout: 5000 }