Рефлексия и интроспекция в TypeScript: как код смотрит на себя

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

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

Начать курс

Рефлексия и интроспекция в TypeScript: как код смотрит на себя

Представьте себе программу, которая способна анализировать саму себя в рантайме. Она может узнать, какие типы у её переменных, какие свойства есть у объектов, и даже принять решение на основе этой информации. Звучит как магия? На самом деле это рефлексия и интроспекция — мощные техники метапрограммирования, которые доступны и в TypeScript.

В этой статье мы разберёмся, чем отличается рефлексия от интроспекции, как их использовать в TypeScript, и почему это может быть полезно в реальных проектах. Никакой скучной теории — только живой код и практические примеры.

Что такое интроспекция и рефлексия?

Давайте сразу расставим точки над i. В программировании эти термины часто путают, но разница есть:

  • Интроспекция — это способность программы наблюдать за своей структурой. Вы можете узнать, какие свойства есть у объекта, какой у него тип, но не можете их изменить.
  • Рефлексия — это более продвинутый уровень: программа не только смотрит на себя, но и изменяет своё поведение в рантайме. Например, динамически вызывает методы или создаёт новые экземпляры классов.

В JavaScript, на котором базируется TypeScript, рефлексия реализована через Reflect и Proxy. Но TypeScript добавляет свой слой — статическую типизацию, которая частично «исчезает» при компиляции. Именно здесь и начинается самое интересное.

Интроспекция типов: type guards и typeof

Самый простой способ заглянуть внутрь кода — использовать type guards. Это конструкции, которые проверяют тип данных в рантайме:

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function processValue(value: unknown) {
  if (isString(value)) {
    // TypeScript знает, что value — строка
    console.log(value.toUpperCase());
  } else {
    console.log('Это не строка');
  }
}

Обратите внимание на value is string — это предикат типа. Он подсказывает TypeScript, что после проверки переменная относится к определённому типу. Это чистая интроспекция: мы смотрим на тип, но не меняем его.

Рефлексия через Reflect API

Объект Reflect — это встроенный механизм рефлексии в JavaScript. Он позволяет выполнять действия, которые раньше делались «магическими» методами, но теперь унифицированы. Вот несколько примеров:

const user = {
  name: 'Алексей',
  age: 30,
};

// Интроспекция — получаем список ключей
console.log(Reflect.ownKeys(user)); // ['name', 'age']

// Рефлексия — динамически читаем свойство
const propertyName = 'name';
console.log(Reflect.get(user, propertyName)); // 'Алексей'

// Рефлексия — динамически устанавливаем свойство
Reflect.set(user, 'name', 'Мария');
console.log(user.name); // 'Мария'

Чем Reflect лучше старого доброго user[prop]? Во-первых, он возвращает boolean для операций установки, что удобно для проверок. Во-вторых, он работает корректно с прокси и дескрипторами свойств. Для TypeScript это особенно важно, так как рефлексия позволяет обойти статическую типизацию, когда это действительно нужно.

Декораторы: рефлексия на стероидах

Декораторы — это, пожалуй, самый яркий пример рефлексии в TypeScript. Они позволяют аннотировать классы, методы, свойства и параметры, а затем изменять их поведение. С версии TypeScript 5.0 декораторы стали стандартными (stage 3).

Вот как выглядит простой декоратор для логирования вызова метода:

function logMethod(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Вызван метод ${propertyKey} с аргументами:`, args);
    return originalMethod.apply(this, args);
  };

  return descriptor;
}

class Calculator {
  @logMethod
  add(a: number, b: number): number {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(2, 3); // Вызван метод add с аргументами: [2, 3]

Декоратор @logMethod перехватывает вызов, логирует аргументы и передаёт управление оригинальному методу. Это чистая рефлексия: мы изменили поведение метода в рантайме.

Метаданные: Reflect Metadata

Чтобы рефлексия стала по-настоящему мощной, нужны метаданные. В TypeScript для этого используется библиотека reflect-metadata (экспериментальная, но очень популярная). Она позволяет хранить дополнительную информацию о типах прямо в рантайме.

import 'reflect-metadata';

function Type(type: any) {
  return Reflect.metadata('design:type', type);
}

class User {
  @Type(String)
  name: string;

  @Type(Number)
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const user = new User('Иван', 25);
// Получаем метаданные
const nameType = Reflect.getMetadata('design:type', user, 'name');
console.log(nameType); // [Function: String]

Это уже не просто интроспекция — это полноценная рефлексия. Мы можем узнать тип свойства даже после компиляции, когда TypeScript «забывает» о статических типах. Без метаданных это было бы невозможно.

Практический пример: валидация форм

Давайте соберём всё вместе. Представьте, что вы пишете библиотеку для валидации данных. С помощью рефлексии и декораторов можно сделать её очень гибкой:

import 'reflect-metadata';

// Декоратор для валидации
function Validate(minLength: number) {
  return Reflect.metadata('validate:minLength', minLength);
}

// Функция валидации
function validate(obj: any): boolean {
  const errors: string[] = [];
  for (const key of Reflect.ownKeys(obj)) {
    const minLength = Reflect.getMetadata('validate:minLength', obj, key);
    if (minLength !== undefined && typeof obj[key] === 'string') {
      if (obj[key].length < minLength) {
        errors.push(`Поле ${String(key)} должно быть длиннее ${minLength} символов`);
      }
    }
  }
  return errors.length === 0;
}

class RegistrationForm {
  @Validate(3)
  username: string;

  @Validate(8)
  password: string;

  constructor(username: string, password: string) {
    this.username = username;
    this.password = password;
  }
}

const form = new RegistrationForm('jo', '12345678');
console.log(validate(form)); // false — username слишком короткий

Этот код использует интроспекцию (перебор свойств объекта) и рефлексию (чтение метаданных декораторов). Без этих техник пришлось бы писать отдельные функции для каждого поля — а так мы получили универсальный валида

Блоги

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