Learning Book

Reflect Metadata API

reflect-metadata

Что такое метаданные

reflect-metadata — это полифил для Metadata Reflection API. Позволяет хранить и получать метаданные, связанные с классами и их членами:

npm install reflect-metadata
// В точке входа (main.ts)
import 'reflect-metadata'

Базовый API

import 'reflect-metadata'

class User {
  name: string = ''
  age: number = 0
}

// Устанавливаем метаданные
Reflect.defineMetadata('description', 'Пользователь системы', User)
Reflect.defineMetadata('version', '1.0', User, 'name')

// Получаем метаданные
const desc = Reflect.getMetadata('description', User)
// 'Пользователь системы'

const nameVersion = Reflect.getMetadata('version', User, 'name')
// '1.0'

// Проверяем наличие
Reflect.hasMetadata('description', User) // true

// Получаем ключи метаданных
Reflect.getMetadataKeys(User) // ['description']

design:type — автоматические метаданные TypeScript

При включённом emitDecoratorMetadata: true, TypeScript автоматически генерирует метаданные типов:

import 'reflect-metadata'

class Service {
  @Inject() // декоратор нужен чтобы TypeScript эмитировал метаданные
  userRepository: UserRepository

  @Inject()
  emailService: EmailService
}

// TypeScript генерирует:
// Reflect.metadata('design:type', UserRepository)(Service.prototype, 'userRepository')
// Reflect.metadata('design:type', EmailService)(Service.prototype, 'emailService')

// Получаем тип свойства
const type = Reflect.getMetadata('design:type', Service.prototype, 'userRepository')
console.log(type === UserRepository) // true
Встроенные ключи: design:type — тип свойства/параметра, design:paramtypes — массив типов параметров конструктора/метода, design:returntype — тип возвращаемого значения.

Практический пример: простой DI-контейнер

import 'reflect-metadata'

// Токен для DI
const INJECTABLE_KEY = 'injectable'
const DEPENDENCIES_KEY = 'design:paramtypes'

// Помечаем класс как injectable
function Injectable() {
  return function (constructor: Function) {
    Reflect.defineMetadata(INJECTABLE_KEY, true, constructor)
  }
}

// Контейнер
class Container {
  private registry = new Map<string, any>()

  register(token: string, constructor: new (...args: any[]) => any) {
    this.registry.set(token, constructor)
  }

  resolve<T>(constructor: new (...args: any[]) => T): T {
    // Получаем типы зависимостей из метаданных
    const dependencies: any[] =
      Reflect.getMetadata(DEPENDENCIES_KEY, constructor) || []

    // Рекурсивно разрешаем зависимости
    const resolvedDeps = dependencies.map(dep => this.resolve(dep))

    return new constructor(...resolvedDeps)
  }
}

@Injectable()
class Logger {
  log(message: string) {
    console.log(`[LOG] ${message}`)
  }
}

@Injectable()
class UserService {
  constructor(private logger: Logger) {}

  createUser(name: string) {
    this.logger.log(`Создан пользователь: ${name}`)
    return { name }
  }
}

const container = new Container()
const userService = container.resolve(UserService)
userService.createUser('Иван')

NestJS строит весь свой DI-контейнер на основе reflect-metadata:

// NestJS под капотом делает примерно это:

@Controller('users')  // Reflect.defineMetadata('path', 'users', UserController)
export class UserController {
  constructor(private userService: UserService) {}
  // design:paramtypes → [UserService]

  @Get(':id')  // Reflect.defineMetadata('method', 'GET', UserController.prototype, 'findOne')
  findOne(@Param('id') id: string) {}
}

// При инициализации NestJS:
// 1. Читает metadata 'path' → создаёт роут
// 2. Читает design:paramtypes → внедряет зависимости
// 3. Читает 'method' на методах → регистрирует HTTP-обработчики