
«Крутую ты штуку придумал, Стёпа», — сообщил мне коллега, осознав рассказанную ему идею. Надеюсь это действительно так, хоть и не скажу, что в том, о чём далее пойдёт речь, есть что-то безумно новаторское, однако, на мой взгляд, интерес данный материал всё же представляет.
Сегодня поговорим о применении интроспекции в разработке веб-интерфейсов, немного пошаманим с обобщённым программированием и изобретём велосипед в 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} />
Ну и саму таблицу сделать по такому же принципу тоже возможно.
Подводя итоги
Хочется сказать, что штука получилась интересная, но пока непонятно, что с ней делать дальше. Если вам было интересно или есть какие-либо предложения, пишите в комментариях, а пока до новых встреч! Спасибо за внимание!
ссылка на оригинал статьи https://habr.com/ru/post/535920/
Добавить комментарий