TypeScript - 属性 装饰器作为类型保护

TypeScript - property decorator as type guard

我想知道是否可以在 TypeScript 中使用 属性 装饰器 作为 type guard 以确保 属性 不是 null |未定义.

让我们假设有以下示例。

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

请注意,由于严格的编译器设置 (strictNullChecks),必须编写string | undefined。因此,仅使用 string 作为类型是不可能的。我知道 non-null-assertion 可以像这样使用:public myProperty!: string。但这实际上是装饰器应该处理的。

最后,装饰器主要检查 属性 是否设置在特定时间点,如果没有则抛出错误。这种检查显然不会在构建时进行,否则就不需要这种方法。它是在不久之后执行的 - 例如在像 Angular 这样的框架的 lifcylce hook 中。我知道 constructor 本身的类型检查是不正确的。我可以接受这一点。如果检查成功,属性 的类型应该缩小到 string,因此您可以安全地使用它,例如this.property.split(" ").

我想知道这样的事情在理论上是否可行?提前致谢。

不,目前在 TypeScript 中这是不可能的;装饰者不会改变他们装饰的东西的类型。在 microsoft/TypeScript#4881 中有一个允许这样做的长期建议,但该问题对于此类功能的确切工作方式存在重大分歧。

更重要的是,在 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 甚至使用装饰器之前,不太可能对装饰器在 TypeScript 中的工作方式进行任何更改。看起来装饰器已经在第 2 阶段停滞了很长时间(至少……三、四年?)。所以现在,我不希望有任何改变。


那么,如果您需要此功能,您可以做什么呢?好吧,您总是可以退回到非装饰器世界,而是使用一个函数来修改传递给它们的 class 构造函数。例如(不确定这是否是一个好的实现):

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 })
    );
}

如果您在 class 构造函数和 属性 名称上调用 isRequired(),生成的构造函数将在 属性 生成具有非空值的实例,在至少在你调用实例 init() 之后(作为一些生命周期钩子的例子):

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