Analyze classes by bones or introspect types in Typescript – InformTFB

Analyze classes by bones or introspect types in Typescript

Analyze classes by bones or introspect types in Typescript

What do we know about introspection?

Wikipedia says that this is the ability to query the type and structure of an object during program execution.

Well that is there is a class:

class Person {
    height: number;
    weight: number;
    bloodPressure: string;
}

Its object is defined by a set of fields, each of which has at least its own type and value.

At the same time, we can get this array at any time of the program execution by calling a function.

const fields = ObjectFields.of(Person)

From theory to practice

I am, of course, a person with a blurry brain, but in this situation I will think in a template way. You can pull out the field names using Object.keys,and type this case throughkeyof. Next, using the keys as indices, the obtained values and the data on them.
Having passed through this information, you can Express your conclusions as follows. Let’s start with a simple description of a certain type that characterizes the object’s field.

interface IObjectField<T extends object> {
    readonly field: keyof T;
    readonly type: string;
    readonly value: any;
}

If you think about it, you can see that this is very similar to FieldInfo. However I realized this at the time of writing this article 🙂
And now is a good time to remember that Typescript is not. NET. For example, you can only use factories to create instances of an object in the context of generalized programming . That is, as in C# will not work.
If you describe the constructor of a certain class, you will get something like the following.

interface IConstructor<T> {
    new(...args: any[]): T;
}

Well, let’s try to create a class introspection tool that meets the following requirements::

  1. All we have at the input is the constructor of the class being studied.
  2. At the output, we get an array of objects of the type IObjectField
  3. The generated data is a constant.

Now the reasoning at the beginning of this section has been translated into 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);
    }
}

Let’s try to “read” the Person class and display the data on the screen.

const fields = new ObjectFields(Person);
console.log(fields);

However, instead of the expected output, we got an empty array.

How so? Everything was compiled and worked without errors. However, the fact is that the resulting array is constructed using Object.keys, and since Javascript works in runtime, then what object we put in, we will get such a set of keys. And the object is empty, so the information about types that we tried to extract was lost somewhere. To “return” it, you need to initialize the class fields with some initial values.

class Person {
    height: number = 80;
    weight: number = 188;
    bloodPressure: string = '120-130 / 80-85';
}

YES! we got what we wanted.

We will also test a more complex situation.

class Material {
    name = "wood";
}

class MyTableClass {
    id = 1;
    title = "";
    isDeleted = false;
    createdAt = new Date();
    material = new Material();
}

The result exceeded expectations.

And what to do about it?

The first thing that came to mind: CRUD applications on react can now be written by implementing generalized components. For example, you need to make a form to insert into a table. Please, no one forbids doing something like this.

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>
    );
}

And then use this component like this.

<TypedForm
    type={Person} />

Well, it is also possible to make the table itself on the same principle.

Conclusion

I want to say that the thing turned out to be interesting, but it is still unclear what to do with it next. If you were interested or have any suggestions, write in the comments, and in the meantime, see you again! Thanks for your attention!

Valery Radokhleb
Valery Radokhleb
Web developer, designer

Leave a Reply

Your email address will not be published. Required fields are marked *