TypeScript Version: 2.4.1
The following code aims to restrict the decorator decorate to members of a class inheriting from Base. However, it seems that K ends up only including members in Base, not in the inherited class. (This is a minimal reproducible example for other cases, e.g., restricting the decorator to methods within subclasses of Base of a certain return type.)
Code
abstract class Base {
base() { return 1; };
}
type ProtoOf<T> = Pick<T, keyof T>;
function decorate<T extends Base, K extends keyof ProtoOf<T>, F extends T[K]>() {
return (proto: ProtoOf<T>, propertyKey: K, descriptor: TypedPropertyDescriptor<F>) => {
// Do stuff.
};
}
class Test extends Base {
@decorate()
bar(): boolean {
return false;
}
}
Expected behavior:
No errors when applying @decorate() to bar().
Actual behavior:
Error: [ts] Argument of type '"bar"' is not assignable to parameter of type '"base"'.
You can work around it by returning a generic decorator from your decorator factory.
function decorate() {
return <T extends Base, K extends keyof T, F extends T[K]>
(proto: ProtoOf<T>, propertyKey: K, descriptor: TypedPropertyDescriptor<F>) => {
// Do stuff.
};
}
class Test extends Base {
@decorate() bar(): boolean {
return false;
}
}
I think this behavior is correct since it is equivalent to writing
function decorate<T extends Base, K extends keyof ProtoOf<T>, F extends T[K]>() {
return (proto: ProtoOf<T>, propertyKey: K, descriptor: TypedPropertyDescriptor<F>) => {
// Do stuff.
};
}
const decorator = decorate();
class Test extends Base {
@decorator bar(): boolean {
return false;
}
}
@aluanhaddad thanks for the update! Your solution does address the issue in my earlier example, though I guess the actual problem I was having had to do with decorator arguments:
function decorate<T extends Base>(property: keyof T) {
return <U extends T, K extends keyof U, F extends U[K]>
(proto: ProtoOf<U>, propertyKey: K, descriptor: TypedPropertyDescriptor<F>) => {
// Do stuff.
};
}
class Test extends Base {
@decorate('foo') bar(): boolean {
return false;
}
foo(): boolean { return false; }
}
So the decorator works on bar() fine now, but is failing with this error: [ts] Argument of type '"foo"' is not assignable to parameter of type '"base"'. Basically, is there a good way of having the decorator parameters be generic w.r.t. the decorated class?
On another note, if bar() is marked private, then the original error returns - it seems that in this case, the decorator is only able to access public properties? My gut feeling is this is a limitation we'd have to deal with.
Just nothing that this function decorate<T extends Base>(property: keyof T) does not have any place to infer T, you can not infer a type from a name of one of its properties. and remember decorate is a factory that returns a function that will be used to decorate. so it is equivalent to decorate(property: keyof Base), which means you can only decorate properties that have the same name as ones in Base.
So @aluanhaddad's suggestion seems like the correct solution here.
Your explanation makes sense. Though the issue still stands that it doesn't seem currently possible to have decorate properties work off the decorated class. It would be great if there was a way to give "context" to decorator properties as to what exact object they're decorating.
@vaskevich I'm not sure if I understand what you are trying to achieve correctly, but you can capture a string literal type parameter when the decorator factory is applied and then subsequently validate that this property exists on the class with the decorated method.
So, going back to your example, you can validate that a foo member exists on the decorated class and we can even place constraints on its type. For example, in the following, Test must have a callable member foo that has the same return type as the decorated member.
(Warning these types are pretty hard to read and I experienced several language service crashes in VS code due to recursion while working them out.)
```ts
type ProtoOf
function decorate
return <
T extends Base & {[P in CK]: G},
K extends keyof T,
F extends T[K] & G,
G extends ((...args: {}[]) => R),
R>(
proto: ProtoOf
propertyKey: K,
descriptor: TypedPropertyDescriptor
// Do stuff.
};
}
class Test extends Base {
@decorate('foo') bar(): boolean {
return false;
}
foo(): boolean {return false;}
}
```
The way this works is by capturing a type for the argument to the factory and using that argument to define the expected shape of the object that will be decorated. The declaration ofGand the intersection type used to describe the target of the decorator was an experiment that seemed to work. The intent was that if we changefooto return a type not assignable to the return type ofbar`, we will get an error at the decorator application site.
Note that the the declaration of T is provided, as in my previous example, by the decorator and not the decorator factory.
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.
Thanks for the replies - I haven't been able to take a look at this again yet, but will open a new issue if there's a specific defect here (which doesn't seem to be the case).
Most helpful comment
Just nothing that this
function decorate<T extends Base>(property: keyof T)does not have any place to inferT, you can not infer a type from a name of one of its properties. and remember decorate is a factory that returns a function that will be used to decorate. so it is equivalent todecorate(property: keyof Base), which means you can only decorate properties that have the same name as ones inBase.So @aluanhaddad's suggestion seems like the correct solution here.