I am trying to generate a d.ts
declaration file for my project.
There are two classes that do calculate heavy logic, and the remaining classes inherit from them. Properties on the subclasses are not defined on the object itself but rather on a getter
named defaults
within the class and defined in runtime.
Base Models
abstract class BaseModel {}
abstract class Model extends BaseModel {}
Inherited Model
class Example extends Model {
get defaults() {
return {
someProp: 1,
anotherProp: 2
}
}
}
My code works perfectly but there is no autocomplete for the dynamically added properties. I tried adding the following to the d.ts
file to make it aware of the dynamically properties but it didn't seem to work.
index.d.ts
class Model<T> extends BaseModel {
[P in keyof T]: T[P]
}
type ExampleType = {
someProp : number
anotherProp : number
}
class Example extends Model<ExampleType> {}
How can I add property definition to inherited classes without having to manually define them?
You can't do this directly, you can do it if you create the class using an extra function which will mutate the creating class to add the proeprties:
type ReplaceInstanceType<T extends new (...args: any[])=> any, TNewInstance> =
T extends new (...args: infer U)=> any ?
new (...args: U) => TNewInstance :
never;
function createModel<T extends new (...args: any[])=> any>(modelClass: T) :
ReplaceInstanceType<T, InstanceType<T> & InstanceType<T>['defaults']> {
return modelClass as any;
}
Note this uses the 3.0 feature Tuples in rest parameters and spread expressions
for a 2.9, 2.8 version of ReplaceInstanceType
see this answer
We can use this in one of two ways:
Use the output of createModel
directly:
const Example = createModel(class extends Model {
constructor (data: string) {
super();
// this.anotherProp // Not ok we can't access teh extra fields inside the class
}
get defaults() {
return {
someProp: 1,
anotherProp: 2
}
}
});
new Example(``).anotherProp // we can create the class and access the extra fields
new Example("").someProp
This has the disadvantage that the extra fields are not usable from within the class itself, which may be an issue in some situation.
The second usage is to use createModel
in the extends clause of the new class and only define the defaults in the class we pass to extends, adding extra methods in the outer class:
class Example extends createModel(class extends Model {
get defaults() {
return {
someProp: 1,
anotherProp: 2
}
}
}) {
constructor(data: string) {
super();
this.anotherProp // new properties are accesible
}
};
new Example(``).anotherProp // we can create the class and access the extra fields
new Example("").someProp
This methods has the disadvantage of actually creating 2 classes the outer one which we actually use and the inner one we use to add the defaults property.
Edit
Since typescript actually does a very good job with strongly typing and validating key names you could also consider using get/set
methods instead, you get good code completion and type validation but it is a bit more verbose:
abstract class BaseModel {
abstract get defaults();
get<K extends keyof this['defaults']>(name: keyof this['defaults']): this['defaults'][K]{
return this[name] as any;
}
set<K extends keyof this['defaults']>(name: keyof this['defaults'], value: this['defaults'][K]) : void{
this[name] = value;
}
}
class Example extends BaseModel {
get defaults() {
return {
someProp: 1,
anotherProp: 2
}
}
};
new Example().get('anotherProp')
new Example().get("someProp")
new Example().set("someProp", 1) //
new Example().set("someProp", "1") // error wrong type
new Example().get("someProp2") // error