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 библиотеки
declare namespace MyLib {
interface Options {
timeout: number
retries: number
// Добавляем своё поле:
customHeaders?: Record<string, string>
}
}Паттерн для обёртки сторонней библиотеки с расширенными типами:
// my-axios.ts
import axios from 'axios'
declare module 'axios' {
interface AxiosRequestConfig {
_retry?: boolean
_retryCount?: number
}
}
export default axiosdeclare module должен быть модулем — содержать хотя бы один import или export {}.declare global { }..d.ts с аугментациями должны быть в include вашего tsconfig.expect.extend@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 позволяет добавлять новые типы в уже существующие модули — включая сторонние библиотеки — без изменения их исходного кода. Это мощный инструмент, когда нужно расширить типы 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 библиотеки
declare namespace MyLib {
interface Options {
timeout: number
retries: number
// Добавляем своё поле:
customHeaders?: Record<string, string>
}
}Паттерн для обёртки сторонней библиотеки с расширенными типами:
// my-axios.ts
import axios from 'axios'
declare module 'axios' {
interface AxiosRequestConfig {
_retry?: boolean
_retryCount?: number
}
}
export default axiosdeclare module должен быть модулем — содержать хотя бы один import или export {}.declare global { }..d.ts с аугментациями должны быть в include вашего tsconfig.expect.extend@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).