← TypeScript/Module Augmentation: расширение чужих типов#243 из 430← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: TypeScript setТермин: TypeScriptМаршрут: старт с нуля
← НазадДалее →

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 }

    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 }

    Задание

    Реализуй функцию augmentPrototype(TargetClass, methods), которая добавляет методы из объекта methods на прототип TargetClass (только если метода ещё нет). Затем расширь класс EventTarget методами onceOn(event, handler) (подписка, которая автоматически отписывается после первого вызова) и hasListeners(event) (возвращает true/false). Используй augmentPrototype для этого расширения.

    Подсказка

    В augmentPrototype: Object.keys(methods).forEach(key => { if (!TargetClass.prototype[key]) TargetClass.prototype[key] = methods[key] }). В onceOn: создай wrapper = (data) => { handler(data); this.off(event, wrapper) }, затем this.on(event, wrapper). В hasListeners: return !!(this._listeners[event] && this._listeners[event].length > 0).

    Загружаем среду выполнения...
    Загружаем AI-помощника...