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 thenevertype is the bottom type assignable to any other type. This is exactly the opposite of theunknowntype. So() => unknownis not assignable to theget()method type, and the compiler correctly balks.As an example, if you can call
worker.get<string>()and get aPromise<string>, and callworker.get<number>()and get aPromise<number>, that means thatworker.get()at runtime somehow produces aPromisewhich is both astringand anumber. This is impossible. There are nostring & numbervalues. It's equivalent tonever.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() => neverand<R>() => Promise<R>. The easiest approach there is to use an unsafe type... theanytype:This is the same as yours except that I use the
ThisParameterTypeutility type to compute the type you referred to asThisinstead of trying to infer it.Yes,
anyis unsafe, but(...args: any) => anymatches just about any function without worrying about the arguments or return type. Trying to make anany-free version is possible, but then it won't play nicely with already-unsafe types like<R>() => R.Playground link to code