TypeScript - property decorator as type guard

1.1k Views Asked by At

I would like to know if it's possible in TypeScript to use a property decorator as a type guard in order ensure that the property is not null | undefined.

Let's assume having the following example.

class MyClass {
   @isRequired()
   public myProperty: string | undefined;
} 

Note that string | undefined has to be written because of strict compiler settings (strictNullChecks). Therefore just using string as the type is not possible. I know that a non-null-assertion could be used like this: public myProperty!: string. But that's what actually the decorator should take care of.

In the end the decorator basically checks if the property is set at a specific point in time and throws and error if not. This check is obviously not done at construction time, because otherwise this approach would not be needed. It's performed shortly after - e.g. in a lifcylce hook of a framework like Angular. I know that the type checking would not be true within the constructor itself. I could accept this though. If the check succeeds the type of the property should be narrowed down to string and therefore you are able to use it safely like e.g. this.property.split(" ").

I am wondering if something like this is theoretical possible? Thanks in advance.

1

There are 1 best solutions below

0
On BEST ANSWER

No, this is not currently possible in TypeScript; decorators do not mutate the types of the things they decorate. There is a longstanding suggestion to allow this in microsoft/TypeScript#4881, but that issue has significant disagreement about exactly how such a feature would work.

More importantly, it is unlikely any changes to how decorators work in TypeScript will be made until the decorators proposal reaches Stage 3 of the TC39 process for introduction to JavaScript. Generally speaking, TypeScript only tries to support potential JavaScript features once they are relatively stable candidates for inclusion. Decorators were one of the earlier features of TypeScript, but adopting features so early had drawbacks; at some point, decorators might make it into JavaScript in a significantly different form to how TypeScript originally anticipated, and then TypeScript will have to make breaking changes. Right now you have to enable the --experimentalDecorators compiler option even to use them. And it looks like decorators have been stalled in Stage 2 for a long time now (at least... three, four years?). So for now, I wouldn't expect any change.


So what can you do instead if you need this functionality? Well, you could always fall back to a non-decorator world and instead use a function that modifies class constructors passed into them. For example (not sure if this is a good implementation or not):

function isRequired<
  C extends new (...args: any[]) => any, 
  K extends keyof InstanceType<C>
>(
    ctor: C,
    prop: K
) {
    return class extends ctor {
        constructor(...args: any[]) {
            super(...args);
        }
        init() {
            if (this[prop as keyof this] == null) throw new Error(prop + " is nullish!!");
        }
    } as Pick<C, keyof C> & (new (...args: ConstructorParameters<C>) =>
        ({ [P in keyof InstanceType<C>]:
            P extends K ? NonNullable<InstanceType<C>[P]> : InstanceType<C>[P]
        } & { init(): void })
    );
}

If you call isRequired() on a class constructor and a property name, the resulting constructor will make instances with non-nullable value at that property, at least after you call init() on the instance (as an example of some lifecycle hook thingy):

const MyClass = isRequired(class MyClass {
    public myProperty: string | undefined;
}, "myProperty");
type MyClass = InstanceType<typeof MyClass>;

const myClass = new MyClass();
myClass.myProperty = "okay";
myClass.init();
console.log(myClass.myProperty.toUpperCase()); // OKAY

const badMyClass = new MyClass();
badMyClass.init(); // myProperty is nullish!!
console.log(badMyClass.myProperty.toUpperCase()); // NEVER GET HERE

Playground link to code