1

Note: I have updated the question to hopefully make things clearer.

I am trying to instantiate classes through a proxy object called elemTypes. This object is a list of references to classes that I want to use to programmatically build objects based on a DOM-like tree structure. I am very new to TypeScript, so I'm hoping that I am just missing something simple here?

Here is the code:

type elemDef = {
    type: string,
    name: string,
    childNodes: object[]
}

class Elem {
    childNodes: object[]
    constructor (childNodes: object[]) {
        this.childNodes = childNodes
    }
}

class Div extends Elem {
    constructor (childNodes: object[]) {
        super(childNodes)
    }
}

class Form extends Elem {
    constructor (childNodes: object[]) {
        super(childNodes)
    }
}

class Input extends Elem {
    name: string
    constructor (childNodes: object[], nodeDef: elemDef) {
        super(childNodes)
        this.name = nodeDef.name
    }
}

const div = {
    type: 'Div',
    childNodes: [
        {
            type: 'Form',
            childNodes: [
                {
                    type: 'Input',
                    name: 'username',
                    childNodes: []
                },
                {
                    type: 'Input',
                    name: 'password',
                    childNodes: []
                },
            ]
        }
    ]
}

// I expect that I am doing this part wrong:
const elemTypes = {
    Div,
    Form,
    Input
}

const makeElem = (nodeDef: elemDef) => {
    const childNodes:object[] = []

    nodeDef.childNodes.forEach((childNodeDef: object): void => {
        const element = new elemTypes[childNodeDef.type](childNodeDef)
        childNodes.push(element)
    })

    const element = new elemTypes[nodeDef.type](nodeDef)

    return element
}

// This line just demonstrates that the code is working:
console.log(JSON.stringify(makeElem(div), null, 2))

The output of the line above is:

{
  "childNodes": {
    "type": "Div",
    "childNodes": [
      {
        "type": "Form",
        "childNodes": [
          {
            "type": "Input",
            "name": "username",
            "childNodes": []
          },
          {
            "type": "Input",
            "name": "password",
            "childNodes": []
          }
        ]
      }
    ]
  }
}

The issue I am having, is that SublimeText 3 is giving me this error:

Element implicity has a type 'any' type because the type
'{ Div: typeof Div; Form: typeof Form; Input: typeof Input };
 has no index signature;

I have tried figuring this out by reading the TypeScript documentation and looking at some StackOverflow answers on similar questions, but I can not seem to find out what I am doing wrong here.

But does anyone know if there is a way to define the meta object that will stop be having this implicit any error in my IDE? (Note: I do not want to turn implicit errors off in my .ts-config.js file.)

I do not want to turn off implicit any rule in my ts-config.

Any ideas appreciated.

f1lt3r
  • 2,176
  • 22
  • 26

2 Answers2

1

It worked in javascript so it must work in (a more convoluted way) Typescript as well.


If "I don't know what you are trying to achieve, but this is how you can do it!" looks like an acceptable start for an answer, keep reading.

Do all the following changes before tackling the main problem.

//Be specific. Change all the "object" type annotations to "elemDef". Doing 
//so, the compiler will be able to validate if the type of an argument you are    
//trying to pass to your constructors contains all the properties existent 
//in an "elemDef" object.

type elemDef = {
    type: string;
    name?: string; //<- not all elements have a name, make it optional
    childNodes: elemDef[]; //<- you have a recursive structure.
}

class Elem { 
    readonly type: string;//<- the type of an element is not supposed to change
    childNodes: elemDef[];
    constructor (childNodes: elemDef[]) {
        this.childNodes = childNodes;
        this.type = "Elem";
    }
}


class Div extends Elem {
    readonly type: string;
    constructor (childNodes: elemDef[]) {
        super(childNodes);
        this.type = "Div";
    }
}

class Form extends Elem {
    readonly type: string;
    constructor (childNodes: elemDef[]) {
        super(childNodes);
        this.type = "Form";
    }
}

The type of the parameter nodeDef in the Input constructor must be elemDef & { name : string } so it will not accept as argument any elemDef that does not have a name.

class Input extends Elem {
    readonly type: string;
    name: string;
    constructor (childNodes: elemDef[], nodeDef: elemDef & { name : string }) {
        super(childNodes);
        this.name = nodeDef.name;
        this.type = "Input";
    }
}

So, what is still missing?

This is the index signature of an object:

{ [key: type1]: type2 }

The compiler is inferring that the elemTypes has type:

const elemTypes: {
    Div: typeof Div;
    Form: typeof Form;
    Input: typeof Input;
}

So it is necessary to provide a signature that maps from string to a function, but then when you do it, the compiler will tell you that you can use the new operator to call a function which lacks a constructor signature. Starting with the second problem:

//this is a type that defines a signature for a generic constructor for elements
type ElementConstructor<T> = {
    new(childNodes: elemDef[]): T;
}

Now we provide a signature and use the ElementConstructor type to cast every class inside the const elemTypes :

const elemTypes : { [index: string]: ElementConstructor<Elem> } = {
    Div: Div as ElementConstructor<Div>,
    Form: Form as ElementConstructor<Form>,
    Input: Input as ElementConstructor<Input>
};

And finally

Just make some adjustments in the makeElement function:

// In your original snippets, you were passing objects to constructor in 
// the forEach but all your constructors take arrays as arguments 
const makeElem = (nodeDef: elemDef) => {
    const childNodes: elemDef[] = [];

    nodeDef.childNodes.forEach((childNodeDef: elemDef): void => {
        const element = new elemTypes[childNodeDef.type]([childNodeDef]); // <- put inside an array
        childNodes.push(element);
    });

    const element = new elemTypes[nodeDef.type]([nodeDef]);  //<- put inside an array
    return element;
}
  • Fantastic! Thanks for taking the time to look into this. The solution seems like a lot of extra work to make something simple. Perhaps this is just what is required to use TypeScript to lock things down. Perhaps this is because my approach is wrong? Is there a better way to do what I am doing that does not require me to build an ElementConturctor type? Thanks again for the wonderful feedback. I really appreciate you taking the time to show me how to set things up. Kudos @betadeveloper – f1lt3r Nov 22 '17 at 15:57
  • This is a just a hacky thing to output the expected result and surely must exist a better way. In fact, if you **don't change anything** in your original sample and try to instantiate **_outside_** the function - new elemTypes["Div"]([...])-you'll see that it works(!?). Also, the typescript module includes definitions to all interfaces for all html elements ... maybe you don't need to implement any on your own –  Nov 22 '17 at 17:16
-1

Not entirely sure, but maybe you can try something like this and see if it helps?

const proxy: {person: Being} = {
    person: Being
}
realharry
  • 1,555
  • 8
  • 12
  • Thanks for the suggestion, I tried this but it did not work. I have updated the question, perhaps that makes things clearer? – f1lt3r Nov 18 '17 at 21:47