Разбираем классы по косточкам или интроспектируем типы в Typescript

от автора

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

Хорошо, попробуем создать инструмент для интроспекции класса, который бы удовлетворял следующим требованиям:

  1. Всё, что у нас есть на входе — это конструктор изучаемого класса.
  2. На выходе мы получаем массив объектов типа IObjectField
  3. Сгенерированные данные — неизменяемы.

Вот теперь рассуждения в начале раздела переведены на язык 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *