Open danielearwicker opened 9 years ago
@rbuckton can you boil this down to something non-decorator-experts can understand? :confused:
Basically the idea is that you have a generic decorator.
function blah<O>(x: O, y: string) {
// ...
}
class C {
firstName = "Homer";
@blah
fullName: string;
}
That's all fine and dandy, but it doesn't really do much for you. There's nothing you can do with O
. So you instead take a callback:
function blah<O>(f: (obj: O) => any) {
// ...
}
class C {
firstName = "Homer";
@blah<C>(c => "I am evil " + c.firstName + ".")
fullName: string;
}
Now you're able to decorate using a callback that is aware of the shape of the prototype being decorated. So it would be valid to write c.firstName
as above, but not c.age
, which is desirable here.
So this works, but it's a little annoying because you need to specify C
as a type argument. @danielearwicker wants type inference here from the class prototype.
The problem is that this is akin to the following:
let f: <T>(callback: (x: T) => void) => (y: T) => void;
// This errors on 'x.a', since 'T' is inferred as '{}'
f(x => x.a)({ a: 100 });
The issue is we only make inferences once at the invocation site of a generic function. I'm not sure what we would do here to fix this.
did this issue have any update ? I have the same problem about this
@RyanCavanaugh, as you seem to be the first line of defense in the issues, I wanted to ping this suggestion back to your attention. It hasn't seemed to have made it into your more recent suggestion backlog slogs and I think it would shore up a major gap in typesafety for Typescript decorators.
@nicholasguyett I'll try to get this on the next one, but the office will be pretty empty for the next two weeks or so. Feel free to ping me again - thanks!
It would also be nice to infer property type from decorator usage, for example:
class SomeClass {
@initialize(() => 321) // function return number, it is expected to be type of property
private prop
}
@RyanCavanaugh is this open for pull requests? This feature would be amazing for a project I'm working on and would be interested in contributing if possible.
Missing type safety is the main reason I advice against using decorators wherever possible. My projects are pretty type-safe (at compile time) and I do not want to undermine that type-safety by using decorators.
Thus I am very interested in this feature.
As far as I understand, it would also make possible a type-safe lazy value decorator in the following form:
@lazy(() => "the lazy value")
readonly someLazyValue: string
It would link the type of the return value "the lazy value"
to the type of the property (string
). Which is currently not possible. Please correct me if I am wrong.
@cntech for that example, have you considered:
@lazy get someLazyValue() {
return "the lazy value"
}
The decorator would just wrap the getter function, replacing it with an on-demand cache initialiser. The type signature is already right, so no need for extra features in that case.
This is the easiest way to implement my original example above - and is exactly what libraries like MobX do for computed
.
I have considered that but I somehow wanted to get rid of the getter syntax with get
and return
. But you are right, it is an option to write it that way.
Still I feel very limited in freedom if decorators are not 100% type-safe which is why I long for that feature. My lazy value was just an illustration.
I guess I wasn't very clear. Here's how it works:
export function lazy(target: any, key: string, descriptor: PropertyDescriptor) {
const getter = descriptor.get!;
let cache: { value: any } | undefined;
descriptor.get = () => (cache = cache || { value: getter() }).value;
}
The getter
from the original property definition is stored away. Then replaced in the descriptor
with a new version that tries the cache first. Give it a try. It is easy to see it working:
let counter = 1;
class Test {
@lazy get something() {
return `counter is ${counter}`;
}
}
const test = new Test();
counter++;
console.log(test.something); // logs "counter is 2"
counter++;
console.log(test.something); // still logs "counter is 2"
So, I just ran into this when messing around with validation code. I'm applying validation rules with decorators, and it's fine for most cases, but I also have a custom rule for special cases that allows you to write the custom logic right there.
export class Foo {
@validate<string>(val => val.startsWith("foo"), "Must start with 'foo'")
public fooProp: string = "foo";
}
What about type-safety/inference for this kind of situation?
const decorator = (t, k, d) => ({ get () { return this._data[k] } })
type Dummy = { bar: string }
class Foo {
private _data: Dummy
// how to define `decorator()` so that `bar` will infer `string` type
// without setting its type here?
@decorator bar
}
new Foo().bar // `any`, but `string` wanted
Any news on that, guys? In my project I already have a couple of decorators that take this
in form of context, just like the first example here, and if they could infer type from the class they are used in, it would be great.
@DanielRosenwasser Your basic idea of manually passing type works, but there big disadvantage
const f = <T, TKey extends keyof T = keyof T>(
object: T,
key: TKey
): T[TKey] => {
return object[key];
};
// without manually passing type
f({ a: true, b: false }, 'a');
// with manually passing type
f<{
a: boolean;
b: boolean;
}>({ a: true, b: false }, 'a');
When we pass type manually our TKey
isn't resolved as in example without manually passing type:
Good
Bad
@RyanCavanaugh,
I can see that this made it into two discussion meetings, however ran out of time to discuss both times. Any chance for this to make it onto another?
I'm sure I don't need to explain the value of decorators, so getting better typing support for them would help see more widespread use.
I'm also aware that implementing the updating ES spec for decorators is on the road map, does that implementation include better typing support such as noted in this ticket, or are the different spaces?
Would love to see an update on this topic, as I've been hitting these roadblocks, as I'm sure others have been, which hinders the dev experience of using and implementing decorators.
Any update on that?
Cross-referencing to #39903
A decorator can receive arguments, by being defined as a function that accepts parameters and then returns a normal decorator function:
I could use the above example like this:
Not a realistic example in that it's just another way of defining a property getter, but it's enough to demonstrates the general problem:
c
is of typeany
so the access tofirstName
andlastName
is not type-checked, and nor is the result of the expression.We can attempt to address this manually, because decorators can be generic:
But this still doesn't close the loop. The compiler is happy with the following abuse, in which
c
is supposedly astring
but is actually aC
, andfullName
is declared astring
but will actually become anumber
(albeitNaN
I guess):In summary: decorators are passed the values of the meta-information in standard parameters (
target
,key
,value
), but they are not passed the compile-time types of those values in a way that can be used to create fully type-safe decorators.So it would be helpful if in decorator-defining functions with generic parameters, those parameters could somehow map automatically to the types of
target
andvalue
(as given meaning in the__decorate
helper).For big yucks, a straw man in which the compiler recognises a couple of built-in decorators that can appear on the type parameters:
Being decorated in this way, it would not be possible to manually specify type arguments for those type parameters when using the decorator. They would effectively be invisible to the user. If the decorator has its own custom type parameters, they would appear from the outside to be the only parameters (and for clarity should be at the front of the list).