I want to write a TypeScript method decorator that can only be applied to methods with a certain type of first argument. It's a common pattern in the codebase I'm working in to pass around a request context that has handles for database, metrics, logging, etc. I'd like to write a decorator that requires one of these resources in the request context, but is otherwise agnostic to the shape of the request context.
Here's a stylized example:
interface MyResource {
logMetricsEtc(...args: any): void;
}
interface HasResourceINeed {
myResource: MyResource;
}
function myDecorator<TFn extends ((tContext: HasResourceINeed, ...rest: any) => any)>(
_target: object,
key: string | symbol,
descriptor: TypedPropertyDescriptor<TFn>,
): TypedPropertyDescriptor<TFn> | void {
const originalHandler = descriptor.value!;
descriptor.value = function (this: any, context: HasResourceINeed, ...inputs: any) {
context.myResource.logMetricsEtc(...inputs);
return originalHandler.apply(this, [context, ...inputs]);
} as TFn;
}
In use, with strictFunctionTypes
enabled, this decorator causes a compile error when applied to a method that otherwise looks reasonable:
interface ServiceContext {
myResource: MyResource;
otherResource: {
sendMessageEtc(msg: string): Promise<void>;
};
}
class MyBusinessClass {
// This causes a compile error, but shouldn't - the decorator will
// work here at runtime.
@myDecorator
async foo(context: ServiceContext, x: number): Promise<void> {
}
// This example MUST cause a compile error to prevent invalid
// usage - there's no resource available in the first arg for the
// decorator to use.
@myDecorator
async bar(y: string): Promise<void> {
}
}
The undesired compile error looks like this:
Argument of type 'TypedPropertyDescriptor<(context: ServiceContext, x: number) => Promise<void>>' is not assignable to parameter of type 'TypedPropertyDescriptor<(tContext: HasResourceINeed, ...rest: any) => any>'.
Types of property 'value' are incompatible.
Type '((context: ServiceContext, x: number) => Promise<void>) | undefined' is not assignable to type '((tContext: HasResourceINeed, ...rest: any) => any) | undefined'.
Type '(context: ServiceContext, x: number) => Promise<void>' is not assignable to type '(tContext: HasResourceINeed, ...rest: any) => any'.(2345)
I can't reasonably turn off strictFunctionTypes
. Is there a way to write the decorator type to accept foo
but reject bar
?
Presumably you want myDecorator()
's input to be generic in the the type R
of the decorated method's first argument and not necessarily in the type Fn
of the whole method. This will allow you to accept methods whose first parameter is some subtype of R
instead of methods which are subtypes of Fn
(which would imply that their argument must be supertypes of R
by method parameter contravariance and is not the constraint you want to apply).
Maybe like this?
function myDecorator<R extends HasResourceINeed>(
_target: object,
key: string | symbol,
descriptor: TypedPropertyDescriptor<((tContext: R, ...rest: any) => any)>,
): TypedPropertyDescriptor<((tContext: R, ...rest: any) => any)> | void {
const originalHandler = descriptor.value!;
descriptor.value = function (this: any, context: R, ...inputs: any) {
context.myResource.logMetricsEtc(...inputs);
return originalHandler.apply(this, [context, ...inputs]);
};
}
That seems to work:
class MyBusinessClass {
@myDecorator // okay
async foo(context: ServiceContext, x: number): Promise<void> {
}
@myDecorator // error
async bar(y: string): Promise<void> {
}
}
Okay, hope that helps; good luck!