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 class
es, 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!
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
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With