Reputation: 873
There is a github issue on Polymorphic this in static methods and also a related question here. These are discussing how to solve/work around the problem in case of static class methods. However I could not find any reference to solve this problem in case of static class members. We have a model class and this class has a few static members containing maps where the keys are the model fields. We are using property decorators in the inherited classes to tag model fields (the inherited classes containing only field definitions). The decorator is also adding the fields to the static maps defined in the base class. See the code below and in playgound
function field(): any {
return function (target: Base, propertyKey: ModelFields<Base>, _descriptor: PropertyDescriptor) {
if (!target.constructor.fields) {
target.constructor.fields = {};
}
target.constructor.fields[String(propertyKey)] = String(`s_${propertyKey}`);
};
}
class Base {
['constructor']: typeof Base;
static fields: Record<string, string>;
// Actually the above is supposed to be Record<keyof ModelFields<this>, string> but 'this' is not allowed here
}
class A extends Base {
@field()
public propA: string = 'A';
}
class B extends Base {
@field()
public propB: string = 'B';
}
type ModelFields<T extends Base> = Required<Omit<T, keyof Base>>
console.log(B.fields) // B.fields type is not correct here
currently the static fields
is defined as Record<string, string>
but it doesn't tell what keys exists on it although we know that the valid keys are keyof ModelFields<this>
. But obviously this
is not allowed there.
Is there any solution to get the typing of fields
right?
Upvotes: 2
Views: 1183
Reputation: 249506
There are some things you can do to get closer to your desired effect.
The first issue as you discovered is the lack of polymorphic this
in static members. We can replace the field with a method. On a method we can use the generic type to capture the call site type:
class Base {
['constructor']: typeof Base;
static fields: Record<any, any>;
static getFields<TThis extends typeof Base>(this: TThis) {
type I = InstanceType<TThis>;
return this.fields as {
[P in keyof I] : string
}
}
}
The other problem is that there is no way to filter members based on a decorator (these are not represented in the type system). What you could do is add a brand to the type all fields that have the decorator and filter the fields on that brand:
class Base {
['constructor']: typeof Base;
static fields: Record<any, any>;
static getFields<TThis extends typeof Base>(this: TThis) {
type I = InstanceType<TThis>;
return this.fields as {
[P in keyof I as I[P] extends FieldBrand<unknown> ? P : never] : string
}
}
}
declare const fieldBrand: unique symbol;
type FieldBrand<T> =T & { [fieldBrand]?: never }
type UnBrandField<T> = T extends FieldBrand<infer U> ? U: never;
class A extends Base {
@field()
public propA: FieldBrand<string> = 'A';
}
Upvotes: 2