reflect-metadata для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

reflect-metadata для TypeScript: полное руководство с примерами

Добро пожаловать в подробное руководство по библиотеке reflect-metadata для TypeScript. Эта библиотека является фундаментом для многих современных фреймворков, таких как Angular, NestJS и TypeORM. Она предоставляет возможность добавлять и читать метаданные (данные о данных) для классов, методов, свойств и параметров во время выполнения программы. Если вы хотите понять, как работают декораторы и Dependency Injection (DI) в TypeScript, вам необходимо освоить reflect-metadata.

1. Что это и зачем нужна?

reflect-metadata — это полифилл (реализация) для встроенного объекта Reflect, который является частью спецификации ES2016 (ES7). Основная цель библиотеки — предоставить API для работы с метаданными, которые могут быть ассоциированы с различными элементами кода (классами, методами, свойствами). В стандартном JavaScript этот функционал отсутствует, поэтому для TypeScript требуется установка отдельного пакета.

Зачем это нужно? Представьте, что вы пишете фреймворк. Вам нужно знать, какие зависимости требуются для класса, какие маршруты (routes) привязаны к методам контроллера, или какие поля сущности должны быть сериализованы в JSON. Без метаданных вам пришлось бы хранить эту информацию в отдельных конфигурационных файлах или использовать сложные паттерны. reflect-metadata позволяет хранить эту информацию прямо в коде, делая его более самодокументированным и удобным для анализа.

Библиотека тесно связана с декораторами. Декоратор — это функция, которая применяется к классу, методу или свойству. Внутри декоратора вы можете вызвать Reflect.defineMetadata, чтобы сохранить какую-либо информацию, а затем, в другом месте программы, использовать Reflect.getMetadata, чтобы её получить. Это основа для реализации Dependency Injection, ORM (Object-Relational Mapping), валидации и многих других паттернов.

2. Установка

Установка библиотеки производится через npm (Node Package Manager). Убедитесь, что у вас установлен Node.js (версия 12 или выше).

npm install reflect-metadata --save

После установки, библиотеку необходимо импортировать в самом начале вашего входного файла (обычно main.ts или index.ts), чтобы она стала доступна глобально.

import "reflect-metadata";

Важно: Импорт должен быть выполнен до любого другого кода, который использует Reflect. Если вы используете фреймворк (например, Angular), он сделает это за вас автоматически. В противном случае, добавьте эту строку в начало файла.

Также необходимо включить поддержку декораторов и метаданных в файле tsconfig.json:

{
  "compilerOptions": {
    "target": "ES6",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Флаг emitDecoratorMetadata заставляет компилятор TypeScript генерировать метаданные типов для декорированных элементов. Это необходимо для работы многих фреймворков.

3. Быстрый старт — минимальный рабочий пример

Давайте создадим простой пример, который демонстрирует, как добавить метаданные к классу и прочитать их.

import "reflect-metadata";

// 1. Определяем ключ для метаданных
const METADATA_KEY = "my_custom_key";

// 2. Создаем декоратор, который добавляет метаданные
function MyDecorator(value: string) {
    return function (target: any) {
        // target — это конструктор класса
        Reflect.defineMetadata(METADATA_KEY, value, target);
    };
}

// 3. Применяем декоратор к классу
@MyDecorator("Это мой класс")
class MyClass {
    greet() {
        console.log("Привет!");
    }
}

// 4. Читаем метаданные
const metadataValue = Reflect.getMetadata(METADATA_KEY, MyClass);
console.log(metadataValue); // Вывод: "Это мой класс"

В этом примере мы создали декоратор MyDecorator, который с помощью Reflect.defineMetadata сохраняет строку "Это мой класс" для класса MyClass. Затем мы используем Reflect.getMetadata для её получения. Это минимальный, но полностью рабочий пример.

4. Основные методы/функции

Библиотека предоставляет набор статических методов объекта Reflect. Рассмотрим 8 ключевых методов с примерами.

4.1. Reflect.defineMetadata

Сигнатура: Reflect.defineMetadata(metadataKey: any, metadataValue: any, target: Object, propertyKey?: string | symbol): void

Что делает: Определяет (записывает) метаданные для указанной цели (target). Если указан propertyKey, метаданные привязываются к свойству или методу, иначе — к самому объекту (классу).

Пример:

import "reflect-metadata";

class User {
    name: string = "John";
}

// Определяем метаданные для класса
Reflect.defineMetadata("role", "admin", User);
// Определяем метаданные для свойства 'name'
Reflect.defineMetadata("format", "string", User.prototype, "name");

console.log(Reflect.getMetadata("role", User)); // "admin"
console.log(Reflect.getMetadata("format", User.prototype, "name")); // "string"

4.2. Reflect.getMetadata

Сигнатура: Reflect.getMetadata(metadataKey: any, target: Object, propertyKey?: string | symbol): any

Что делает: Возвращает значение метаданных для указанного ключа. Если метаданные не найдены, возвращает undefined. Поиск происходит по цепочке прототипов (наследование).

Пример:

import "reflect-metadata";

class Base {
    greet() {}
}
Reflect.defineMetadata("version", 1, Base);

class Derived extends Base {}

// Поиск идет по цепочке прототипов
console.log(Reflect.getMetadata("version", Derived)); // 1 (унаследовано от Base)

4.3. Reflect.getOwnMetadata

Сигнатура: Reflect.getOwnMetadata(metadataKey: any, target: Object, propertyKey?: string | symbol): any

Что делает: Работает как getMetadata, но не ищет по цепочке прототипов. Возвращает метаданные, определенные непосредственно для данного объекта.

Пример:

import "reflect-metadata";

class Parent {
    greet() {}
}
Reflect.defineMetadata("key", "parent", Parent);

class Child extends Parent {}
Reflect.defineMetadata("key", "child", Child);

console.log(Reflect.getOwnMetadata("key", Child)); // "child"
console.log(Reflect.getOwnMetadata("key", Parent)); // "parent"
console.log(Reflect.getMetadata("key", Child)); // "child" (находит собственное)

4.4. Reflect.hasMetadata

Сигнатура: Reflect.hasMetadata(metadataKey: any, target: Object, propertyKey?: string | symbol): boolean

Что делает: Проверяет, существуют ли метаданные с указанным ключом для цели. Поиск ведется по цепочке прототипов.

Пример:

import "reflect-metadata";

class MyClass {}
Reflect.defineMetadata("isActive", true, MyClass);

console.log(Reflect.hasMetadata("isActive", MyClass)); // true
console.log(Reflect.hasMetadata("nonExistent", MyClass)); // false

4.5. Reflect.hasOwnMetadata

Сигнатура: Reflect.hasOwnMetadata(metadataKey: any, target: Object, propertyKey?: string | symbol): boolean

Что делает: Проверяет, существуют ли метаданные, определенные непосредственно для данного объекта (без учета прототипов).

Пример:

import "reflect-metadata";

class Parent {}
Reflect.defineMetadata("key", 1, Parent);

class Child extends Parent {}

console.log(Reflect.hasOwnMetadata("key", Child)); // false (не определены напрямую)
console.log(Reflect.hasMetadata("key", Child)); // true (унаследованы)

4.6. Reflect.deleteMetadata

Сигнатура: Reflect.deleteMetadata(metadataKey: any, target: Object, propertyKey?: string | symbol): boolean

Что делает: Удаляет метаданные с указанным ключом. Возвращает true, если метаданные существовали и были удалены, иначе false.

Пример:

import "reflect-metadata";

class MyClass {}
Reflect.defineMetadata("temp", "value", MyClass);

console.log(Reflect.getMetadata("temp", MyClass)); // "value"
Reflect.deleteMetadata("temp", MyClass);
console.log(Reflect.getMetadata("temp", MyClass)); // undefined

4.7. Reflect.getMetadataKeys

Сигнатура: Reflect.getMetadataKeys(target: Object, propertyKey?: string | symbol): any[]

Что делает: Возвращает массив всех ключей метаданных, доступных для цели (включая унаследованные).

Пример:

import "reflect-metadata";

class Base {
    method() {}
}
Reflect.defineMetadata("a", 1, Base);
Reflect.defineMetadata("b", 2, Base.prototype, "method");

class Derived extends Base {
    method() {}
}
Reflect.defineMetadata("c", 3, Derived);

const keys = Reflect.getMetadataKeys(Derived);
console.log(keys); // ["c", "a"] (порядок может отличаться)

4.8. Reflect.getOwnMetadataKeys

Сигнатура: Reflect.getOwnMetadataKeys(target: Object, propertyKey?: string | symbol): any[]

Что делает: Возвращает массив ключей метаданных, определенных непосредственно для данного объекта (без учета прототипов).

Пример:

import "reflect-metadata";

class Parent {}
Reflect.defineMetadata("x", 1, Parent);

class Child extends Parent {}
Reflect.defineMetadata("y", 2, Child);

console.log(Reflect.getOwnMetadataKeys(Child)); // ["y"]
console.log(Reflect.getMetadataKeys(Child)); // ["y", "x"]

4.9. Reflect.metadata (декоратор)

Сигнатура: Reflect.metadata(metadataKey: any, metadataValue: any): { (target: Function): void; (target: Object, propertyKey: string | symbol): void }

Что делает: Это встроенный декоратор, который упрощает добавление метаданных. Он возвращает функцию-декоратор, которую можно применить к классу, методу или свойству.

Пример:

import "reflect-metadata";

@Reflect.metadata("description", "Это класс для работы с API")
class ApiService {
    @Reflect.metadata("method", "GET")
    fetchData() {}
}

console.log(Reflect.getMetadata("description", ApiService)); // "Это класс для работы с API"
console.log(Reflect.getMetadata("method", ApiService.prototype, "fetchData")); // "GET"

5. Реальный практический пример (100-200 строк кода)

Давайте создадим простой Dependency Injection (DI) контейнер. Это покажет, как reflect-metadata используется в реальных проектах.

import "reflect-metadata";

// 1. Ключи для метаданных
const INJECTABLE_KEY = Symbol("injectable");
const INJECT_KEY = Symbol("inject");

// 2. Декоратор @Injectable — помечает класс как доступный для DI
function Injectable(): ClassDecorator {
    return (target: any) => {
        Reflect.defineMetadata(INJECTABLE_KEY, true, target);
    };
}

// 3. Декоратор @Inject — указывает, какие зависимости нужны
function Inject(token: string): ParameterDecorator {
    return (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) => {
        // Сохраняем массив зависимостей для конструктора
        const existingInjections: string[] = Reflect.getOwnMetadata(INJECT_KEY, target) || [];
        existingInjections[parameterIndex] = token;
        Reflect.defineMetadata(INJECT_KEY, existingInjections, target);
    };
}

// 4. Класс DI контейнера
class DIContainer {
    private services: Map = new Map();

    // Регистрация сервиса
    register(token: string, instance: any): void {
        this.services.set(token, instance);
    }

    // Разрешение зависимостей и создание экземпляра
    resolve(target: new (...args: any[]) => T): T {
        // Проверяем, помечен ли класс как Injectable
        const isInjectable = Reflect.getMetadata(INJECTABLE_KEY, target);
        if (!isInjectable) {
            throw new Error(`Class ${target.name} is not marked as @Injectable`);
        }

        // Получаем метаданные о зависимостях конструктора
        const injections: string[] = Reflect.getOwnMetadata(INJECT_KEY, target) || [];

        // Создаем массив зависимостей
        const dependencies                    

Рекомендаций