TypeScript - passing a class as an argument, and reflection

我只是一个虾纸丫 提交于 2019-12-23 03:00:40

问题


I am writing a generic unmarshaller. It converts graph DB data to generated TypeScript (1.8.7) model classes. The input is JSON. The output should be an instance of a model class.
My ultimate goal is to create something like Hibernate OGM, only for Tinkerpop Frames and TypeScript, with REST endpoint in the middle.

What's the right way to pass a class as a parameter and reach it's static members? I want to have something like this:

SomeModel some = <SomeModel> unmarshaller.fromJSON({/*Object from JSON*/}, SomeModel);

I've tried to write a method. Not sure if I am heading in the right direction, feel free to suggest different approaches.

public fromJSON(input: Object, clazz: typeof FrameModel): FrameModel
{
    // This only demonstrates access to Framemodel's metadata 
    // about original Java model classes.
    clazz.graphPropertyMapping;
    clazz.graphRelationMapping;

    let result = {};
    ...
    return result;
}
...

But when I tried to execute this on Plunker, I got execution errors with unuseful stacktrace.

The model superclass looks like this:

/**
 * Things common to all Frames models on the Typescript side.
 */
export class FrameModel
{
    // Model metadata
    static discriminator: string;
    static graphPropertyMapping: { [key:string]:string; };
    static graphRelationMapping: { [key:string]:string; };

    // Each instance needs a vertex ID
    private vertexId: number;
    public getVertexId(): number {
        return this.vertexId;
    }
}

Sample model class:

import {TestPlanetModel} from './TestPlanetModel';
import {TestShipModel} from './TestShipModel';

export class TestGeneratorModel extends FrameModel
{
    static discriminator: string = 'TestGenerator';
    static graphPropertyMapping: { [key:string]:string; } = {
        bar: 'boo',
        name: 'name',
        rank: 'rank',
    };
    static graphRelationMapping: { [key:string]:string; } = {
        colonizes: 'colonizedPlanet',
        commands: 'ship',
    };

    boo: string;
    name: string;
    rank: string;

    public colonizedPlanet: TestPlanetModel[]; // edge label 'colonizedPlanet'

    public ship: TestShipModel; // edge label 'ship'

}

I haven't found much material on reflection and class handling in TypeScript.
I know how I would do this in Java.
I know how I would do this in JavaScript.
I understand that I might achieve similar results with decorators, but having fields or static fields seemed a bit simpler, for generated models.


回答1:


You've maybe already noticed that class members cannot have const keyword. But you could go with static instead. Also member should be public if you want it to be accessible from outside world.

public static graphPropertyMapping: { [key:string]:string; } = {
    bar: 'boo',
    name: 'name',
    rank: 'rank',
};

As for creating result instance:

let result = new clazz();

//copy properties

return result;



回答2:


If I understand you correctly then here's something to help you get started:

interface Model {}

interface ModelData {}

interface MyModelConstructor<M extends Model, D extends ModelData> {
    new(data: D): M;
    // static members
    graphPropertyMapping: any;
    graphRelationMapping: any;
}

class Unmarshaller {
    public fromJSON<T>(input: string | ModelData, ctor: MyModelConstructor<T, ModelData>): T {
        let data: ModelData = (typeof input === "string") ? JSON.parse(input) : input;

        let propertyMapping = ctor.graphPropertyMapping;
        let relationMapping = ctor.graphRelationMapping;

        // do whatever with the mappings

        return new ctor(input);
    }
}

(code in playground)

I don't know how your models look like, so I hope this is close enough.




回答3:


I recently released an enhanced version of the TypeScript compiler that allows exactly what you are expecting: read all (static or not) fields metadata from a class. For example you can write:

interface MyInterface {
    active:boolean;
    description: string;
}

class MyClass {
    id: number;
    name: string;
    myComplexField: MyInterface;
}

function printMembers(clazz: Class) {
    let fields = clazz.members.filter(m => m.type.kind !== 'function'); //exclude methods.
    for(let field of fields) {
        let typeName = field.type.kind;
        if(typeName === 'class' || typeName === 'interface') {
            typeName = (<Class | Interface>field.type).name;
        }
        console.log(`Field ${field.name} of ${clazz.name} has type: ${typeName}`);
    }
}

printMembers(MyClass.getClass());

this is the output:

$ node main.js
Field id of MyClass has type: number
Field name of MyClass has type: string
Field myComplexField of MyClass has type: MyInterface

Of course, if you change the members property access of clazz to statics you will retrieve all static members. These information can be accessed at coding time too, so you can use autocompletion. You can do the same with Interfaces metadata. Simply write MyInterface for example, and access its members. You can find the project here.



来源:https://stackoverflow.com/questions/38624506/typescript-passing-a-class-as-an-argument-and-reflection

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!