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