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