I am trying to get a class method decorator that works just fine with a normal class method to work with a generic class method.
I have this code typescript playground link:
abstract class WorkerBase {
log(...arguments_: unknown[]) {
console.log(arguments_);
}
}
const span =
<
This extends WorkerBase,
Target extends (this: This, ...arguments_: unknown[]) => Promise<unknown>,
>() =>
(target: Target, context: ClassMethodDecoratorContext<This, Target>) =>
async function replacementMethod(
this: This,
...originalArguments: unknown[]
) {
this.log(['called', this.constructor.name, context.name]);
return target.call(this, ...originalArguments);
};
class ExterneWorker extends WorkerBase {
@span()
protected async get<Return>() {
return undefined as unknown as Return;
}
}
And compilation fails with:
Decorator function return type '(this: ExterneWorker, ...originalArguments: unknown[]) => Promise<unknown>' is not assignable to type 'void | (<Return>() => Promise<Return>)'.
Type '(this: ExterneWorker, ...originalArguments: unknown[]) => Promise<unknown>' is not assignable to type '<Return>() => Promise<Return>'.
Type 'Promise<unknown>' is not assignable to type 'Promise<Return>'.
Type 'unknown' is not assignable to type 'Return'.
'Return' could be instantiated with an arbitrary type which could be unrelated to 'unknown'.
The span
decorator works just fine with a non-generic class method, but it fails with a generic one. I don't really understand what TypeScript is telling me. I think something is wrong with the Return
definition in my span decorator definition.
The problem is mostly that the type of the get()
method is <R>() => Promise<R>
, which means that it somehow returns any type the caller wants. Such a method cannot possibly be implemented safely, and according to a sound type system, it is equivalent to () => never
, where the never
type is the bottom type assignable to any other type. This is exactly the opposite of the unknown
type. So () => unknown
is not assignable to the get()
method type, and the compiler correctly balks.
As an example, if you can call worker.get<string>()
and get a Promise<string>
, and call worker.get<number>()
and get a Promise<number>
, that means that worker.get()
at runtime somehow produces a Promise
which is both a string
and a number
. This is impossible. There are no string & number
values. It's equivalent to never
.
If you want to use that type anyway, it means you need your span()
to accept a method of any function type, even those unsafe ones like () => never
and <R>() => Promise<R>
. The easiest approach there is to use an unsafe type... the any
type:
const span = <
Target extends (...args: any) => Promise<any>,
>() =>
(
target: Target,
context: ClassMethodDecoratorContext<ThisParameterType<Target>, Target>
) =>
async function replacementMethod(
this: ThisParameterType<Target>,
...originalArguments: Parameters<Target>
) {
console.log(['called', context.name]);
return target.call(this, ...originalArguments);
};
class ExterneWorker extends WorkerBase {
@span() // okay
async get<Return>() {
return undefined as unknown as Return;
}
}
This is the same as yours except that I use the ThisParameterType
utility type to compute the type you referred to as This
instead of trying to infer it.
Yes, any
is unsafe, but (...args: any) => any
matches just about any function without worrying about the arguments or return type. Trying to make an any
-free version is possible, but then it won't play nicely with already-unsafe types like <R>() => R
.