【问题标题】:How to reuse decorators from within decorators in TypeScript如何在 TypeScript 的装饰器中重用装饰器
【发布时间】:2025-12-21 14:15:11
【问题描述】:

我正在尝试创建一些包装 Angular2 装饰器的功能。我想简化将 CSS 类添加到主机的过程,因此我创建了以下内容:

警告:不适用于 AOT 编译

type Constructor = {new(...args: any[]): {}};

export function AddCssClassToHost<T extends Constructor>(cssClass: string) {
    return function (constructor: T) {
        class Decorated extends constructor {
            @HostBinding("class") cssClass = cssClass;
        }
        // Can't return an inline class, so we name it.
        return Decorated;
    };
}

我还希望能够创建另一个添加特定 CSS 类的装饰器。

/**
 * Decorator to be used for components that are top level routes. Automatically adds the content-container class that is
 * required so that the main content scrollbar plays nice with the header and the header won't scroll away.
 */
export function TopLevelRoutedComponent<T extends Constructor>(constructor: T) {
   // This causes an error when called as a decorator
   return AddCssClassToHost("content-container");
    // Code below works, I'd like to avoid the duplication
    // class Decorated extends constructor {
    //     @HostBinding("class") cssClass = "content-container";
    // }
    // return Decorated;
}

// Called like
@TopLevelRoutedComponent
@Component({
    selector: "vcd-administration-navigation",
    template: `
        <div class="content-area">
            <router-outlet></router-outlet>
        </div>
        <vcd-side-nav [navMenu]="navItems"></vcd-side-nav>`
})
export class AdminNavigationComponent {
    navItems: NavItem[] = [{nameKey: "Multisite", routerLink: "multisite"}];
}

我得到的错误信息是

 TS1238: Unable to resolve signature of class decorator when called as an expression.
 Type '(constructor: Constructor) => { new (...args: any[]): AddCssClassToHost<Constructor>.Decorated; p...' is not assignable to type 'typeof AdminNavigationComponent'.
 Type '(constructor: Constructor) => { new (...args: any[]): AddCssClassToHost<Constructor>.Decorated; p...' provides no match for the signature 'new (): AdminNavigationComponent'

我能够通过创建一个由两者调用的函数来解决它

function wrapWithHostBindingClass<T extends Constructor>(constructor: T, cssClass: string) {
    class Decorated extends constructor {
        @HostBinding("class") cssClass = cssClass;
    }
    return Decorated; // Can't return an inline decorated class, name it.
}
export function AddCssClassToHost(cssClass: string) {
    return function(constructor) {
        return wrapWithHostBindingClass(constructor, cssClass);
    };
}
export function TopLevelRoutedComponent(constructor) {
    return wrapWithHostBindingClass(constructor, "content-container");
}

有没有一种方法可以使第一种样式生效,而无需辅助函数或复制代码?我意识到我的尝试不是很好而且没有任何意义,但我无法理解错误消息。

与(某种)AOT 兼容的版本 因为下面要简单很多,所以不会导致 AOT 编译器崩溃。但是请注意,如果使用 webpack 编译器,自定义装饰器会被剥离,因此它仅在使用 ngc 编译时才有效。

export function TopLevelRoutedComponent(constructor: Function) {
    const propertyKey = "cssClass";
    const className = "content-container";
    const descriptor = {
        enumerable: false,
        configurable: false,
        writable: false,
        value: className
    };
    HostBinding("class")(constructor.prototype, propertyKey, descriptor);
    Object.defineProperty(constructor.prototype, propertyKey, descriptor);
} 

【问题讨论】:

    标签: typescript angular2-decorators


    【解决方案1】:

    装饰器的类型签名中存在一些错误,使其无法满足类型检查器的要求。

    为了修复它们,您必须了解装饰器装饰器工厂之间的区别。

    您可能会怀疑,装饰器工厂只不过是一个返回装饰器的函数。

    当您想通过参数化来自定义应用的装饰器时,请使用装饰器工厂。

    以你的代码为例,下面是一个装饰器工厂

    export type Constructor<T = object> = new (...args: any[]) => T;
    
    export function AddCssClassToHost<C extends Constructor>(cssClass: string) {
        return function (Class: C) {
            
            // TypeScript does not allow decorators on class expressions so we create a local
            class Decorated extends Class {
                @HostBinding("class") cssClass = cssClass;
            }
            return Decorated;
        };
    }
    

    装饰器工厂接受一个参数,该参数自定义它返回的装饰器使用的 css,并将目标替换为(这是一口)。

    但是现在我们想重新使用装饰器工厂来添加一些特定的 css。这意味着我们需要一个普通的老式装饰器,而不是工厂。

    由于装饰器工厂返回的正是我们需要的,我们可以直接调用它。

    使用你的第二个例子,

    export const TopLevelRoutedComponent = AddCssClassToHost("content-container");
    

    现在我们在应用程序上遇到错误

    @TopLevelRoutedComponent
    @Component({...})
    export class AdminNavigationComponent {
        navItems = [{nameKey: "Multisite", routerLink: "multisite"}];
    }
    

    我们必须考虑原因。

    调用工厂函数会实例化其类型参数。

    我们需要一个创建通用装饰器的工厂,而不是返回非通用装饰器的通用装饰器工厂!

    也就是说,TopLevelRoutedComponent 需要是通用的。

    我们可以通过简单地重写我们原来的装饰器工厂,将类型参数移动到它返回的装饰器来做到这一点

    export function AddCssClassToHost(cssClass: string) {
        return function <C extends Constructor>(Class: C) {
    
            // TypeScript does not allow decorators on class expressions so we create a local
            class Decorated extends Class {
                @HostBinding("class") cssClass = cssClass;
            }
            return Decorated;
        };
    }
    

    这是live example

    【讨论】:

    • 效果很好,我几乎很惭愧我没有想到这个
    • 有点微妙。因为您的装饰器返回一个新类而不是就地修改目标,所以新类需要可分配给原始类。大多数装饰器会改变他们的目标并返回void
    • @JuanMendes 只是提醒一下,虽然你对装饰器进行抽象的本能是明智和理性的(保持干燥,促进重用,帮助编写应用程序级模式),它只有在你编写时才有效在 TypeScript 或 JavaScript 中(例如使用 babel-plugin-transform-decorators-legacy)。如果您使用 Angular 的 AOT 语言编写,则不能使用装饰器。
    • 谢谢,我们确实使用 AOT 进行生产,但我还没有运行生产版本。你又是对的。 Error encountered resolving symbol values statically. Function calls are not supported. Consider replacing the function or lambda with a reference to an exported function (position 40:12 in the original .ts file), resolving symbol AddCssClassToHost
    • 而且我确实忘记提及为什么需要将通用 T 移动到内部的解释很棒,这真的是让我失望的原因。
    最近更新 更多