Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript: auto-generated dynamic function names

I have some dynamically generated function names in TypeScript. The only way I can use them now is to cast my objects to <any>. Ex.: <any>myInstance.getDataA(). These functions are dynamically generated based on some rules. Based on the same rules I'd like to generate type-definitions for my classes, but I can not make it work.

original.ts

abstract class Original {

    dynamics = ['getData', 'setData'];

    constructor() {
        // I create functions here dynamically
        this.dynamics.forEach((key) => {
            this[key + this.info] = () => null;
        });
    }

    get info() {
        return 'X';
    }
}

my-class.ts

class MyClass extends Original {
    get info() {
        return 'A';
    }
}

my-other-class.ts

class MyOtherClass extends Original {
    get info() {
        return 'B';
    }
}

something.ts

const myInstance = new MyClass();
console.log(myInstance.getDataA()); // TS2339: Property getDataA does not exist on type: 'MyClass'

const myOtherInstance = new MyOtherClass();
console.log(myInstance.getDataB()); // TS2339: Property getDataB does not exist on type: 'MyClass'

I would like to automatically generate a definition file to define these dynamic properties.

Ex.:

my-class.def.ts

 declare interface MyClass {
    getDataA;
    setDataA
 }

 //my-other-class.def.ts
 declare interface MyClass {
    getDataB;
    setDataB
 }

But I can not find a syntax for my definition files to make it work. Pls ask me if I was not clear, and pls help if you have any idea!

like image 946
Adam Avatar asked Jun 25 '18 14:06

Adam


1 Answers

Edit for 4.1

Using Template literal types and mapped type 'as' clauses we can now do concatenate strings in the type system and create a class that has these properties created dynamically.

function defineDynamicClass<T extends string[]>(...info: T): {
    new (): {
        [K in T[number] as `get${Capitalize<K>}`]: () => unknown
    } & {
        [K in T[number] as `set${Capitalize<K>}`]: (value: unknown) => void
    } & {
        info: T
    }
} {
    return class {
        get info () {
            return info;
        }
    } as any
}
class MyClass extends defineDynamicClass('A', 'B', 'ABAB') {
}
let s =new MyClass();
s.getA();
s.getABAB();
s.setA("")
s.info;

Playground Link

Before 4.1

The within language approach

There is no way to do this within the type system, since we can't perform string manipulation on string literal types. The closest you can get, without external tools, is to create get/set methods that take a string literal type, that will be of the same as that returned by the getInfo method.

function stringLiteralArray<T extends string>(...v: T[]){ return v;}

abstract class Original {
    get(name: this['info'][number]) {
        return null;
    }

    set(name: this['info'][number], value: any) {
        return null;
    }

    get info() : string[]{
        return [];
    }
}

class MyOtherClass extends Original {
    get info() {
        return stringLiteralArray('A', 'B', 'ABAB');
    }
}
class MyClass extends Original {
    get info() {
        return stringLiteralArray('C', 'D', 'DEDE');
    }
}

let s =new MyClass();
s.get('A') // error 
s.get('C') // ok

While this approach is not 100% what you want, form our previous discussions the aim was to have full code-completion for the methods, and this approach achieves this. You get errors if you pass in the wrong value and you get a completion list for the string:

enter image description here

The compiler API approach

A second approach would be to create a custom tool that uses the typescript compiler API to parse the ts files, look for classes derived from Original and generates interfaces containing the methods (either in the same file or a different file) , if you are interested in this I can write the code, but it's not trivial, and while the compiler API is stable I don't think the compiler team takes as much care with backward compatibility as they do with the language (in fact this is the exact statement they make in the documentation page).

If you are interested in such a solution, let me know and I can provide it, but I advise against it.

like image 50
Titian Cernicova-Dragomir Avatar answered Sep 17 '22 20:09

Titian Cernicova-Dragomir