«Крутую ты штуку придумал, Стёпа», — сообщил мне коллега, осознав рассказанную ему идею. Надеюсь это действительно так, хоть и не скажу, что в том, о чём далее пойдёт речь, есть что-то безумно новаторское, однако, на мой взгляд, интерес данный материал всё же представляет.
Сегодня поговорим о применении интроспекции в разработке веб-интерфейсов, немного пошаманим с обобщённым программированием и изобретём велосипед в Typescript, имеющий похожий аналог в .NET.
Что мы знаем об интроспекции?
Википедия гласит, что это возможность запросить тип и структуру объекта во время выполнения программы.
Ну то есть имеется класс:
class Person {
height: number;
weight: number;
bloodPressure: string;
}
Его объект определяется набором полей, каждое из которых имеет, по крайней мере, свой тип и значение.
При этом данный массив мы можем получить в любой момент выполнения программы, вызвав какую-то функцию.
const fields = ObjectFields.of(Person)
От теории к практике
Я, конечно, человек с замыленными мозгами, но в данной ситуации буду мыслить шаблонно. Вытянуть имена полей можно с помощью Object.keys
, а типизировать это дело уже через keyof
. Далее используя ключи, как индексы, получаем значения и данные о них.
Пропустив через себя эту информацию, можно выразить свои выводы следующим образом. Начнём с простого, описав некий тип, характеризующий поле объекта.
interface IObjectField<T extends object> {
readonly field: keyof T;
readonly type: string;
readonly value: any;
}
Если задуматься, то можно увидеть, что это сильно напоминает FieldInfo. Правда я это понял в момент написания статьи :)
И сейчас самое время вспомнить, что Typescript — это не .NET. Например, создавать экземпляры объекта в контексте обобщённого программирования здесь можно только с помощью фабрик. То есть, как в C# не прокатит.
Если описывать конструктор некого класса, то получится приблизительно следующее.
interface IConstructor<T> {
new(...args: any[]): T;
}
Хорошо, попробуем создать инструмент для интроспекции класса, который бы удовлетворял следующим требованиям:
- Всё, что у нас есть на входе — это конструктор изучаемого класса.
- На выходе мы получаем массив объектов типа
IObjectField
- Сгенерированные данные — неизменяемы.
Вот теперь рассуждения в начале раздела переведены на язык Typescript.
class ObjectFields<T extends object> extends Array<IObjectField<T>> {
readonly [n: number]: IObjectField<T>;
constructor(type: IConstructor<T>) {
const instance: T = new type();
const fields: Array<IObjectField<T>> = (Object.keys(instance) as Array<keyof T>)
.map(x => {
const valueType = typeof instance[x];
let result: IObjectField<T> = {
field: x,
type: valueType === 'object'
? (instance[x] as unknown as object).constructor.name
: valueType,
value: instance[x]
}
return result;
});
super(...fields);
}
}
Попробуем "прочитать" класс Person
и выведем данные на экран.
const fields = new ObjectFields(Person);
console.log(fields);
Правда, вместо ожидаемого вывода получили пустой массив.
Как же так? Всё скомпилировалось и отработало без ошибок. Однако дело в том, что результирующий массив строится с помощью Object.keys
, и поскольку в рантайме работает Javascript, то какой объект засунем, такой набор ключей и получим. А объект — пустой, вот и информация о типах, которую мы попытались извлечь, куда-то потерялась. Чтобы её "вернуть", необходимо инициализировать поля класса какими-то начальными значениями.
class Person {
height: number = 80;
weight: number = 188;
bloodPressure: string = '120-130 / 80-85';
}
Вуаля — получили, что хотели.
Также протестируем более сложную ситуацию.
class Material {
name = "wood";
}
class MyTableClass {
id = 1;
title = "";
isDeleted = false;
createdAt = new Date();
material = new Material();
}
Результат превзошёл ожидания.
И что с этим делать?
Первое, что пришло в голову: CRUD приложения на react теперь можно писать, реализуя обобщённые компоненты. Например, нужно сделать форму для вставки в таблицу. Пожалуйста, никто не запрещает делать что-то такое.
interface ITypedFormProps<T extends object> {
type: IConstructor<T>;
}
function TypedForm<T extends object>(props: ITypedFormProps<T>) {
return (
<form>
{new ObjectFields(props.type).map(f => mapFieldToInput(f))}
</form>
);
}
И использовать потом этот компонент вот так.
<TypedForm
type={Person} />
Ну и саму таблицу сделать по такому же принципу тоже возможно.
Подводя итоги
Хочется сказать, что штука получилась интересная, но пока непонятно, что с ней делать дальше. Если вам было интересно или есть какие-либо предложения, пишите в комментариях, а пока до новых встреч! Спасибо за внимание!
Автор: Степан Минин