Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic decorators - could they receive some default type arguments? #2607

Open
danielearwicker opened this issue Apr 3, 2015 · 18 comments
Open
Labels
Domain: Decorators The issue relates to the decorator syntax In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@danielearwicker
Copy link

A decorator can receive arguments, by being defined as a function that accepts parameters and then returns a normal decorator function:

function computed(evaluator: (obj: any) => any) {
    return (obj: any, key: string) => {
        Object.defineProperty(obj, key, { 
            get() { return evaluator(obj); }
        });
    };
}

I could use the above example like this:

class C {
    firstName = "Homer";
    lastName = "Simpson";

    @computed(c => c.firstName + " " + c.lastName) 
    fullName: string;
}

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 type any so the access to firstName and lastName is not type-checked, and nor is the result of the expression.

We can attempt to address this manually, because decorators can be generic:

function computed<O, R>(evaluator: (obj: O) => R) {
    return (obj: O, key: string) => {
        Object.defineProperty(obj, key, { 
            get() { return evaluator(obj); }
        });
    };
}

class C {
    firstName = "Homer";
    lastName = "Simpson";

    @computed<C, string>(c => c.firstName + " " + c.lastName)
    fullName: string;
}

But this still doesn't close the loop. The compiler is happy with the following abuse, in which c is supposedly a string but is actually a C, and fullName is declared a string but will actually become a number (albeit NaN I guess):

@computed<string, number>(c => parseInt(c, 10))
fullName: string;

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 and value (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:

function computed<@decoratorTarget O, @decoratorValue R>(evaluator: (obj: O) => R) {
    ...
}

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).

@danquirk danquirk added the Suggestion An idea for TypeScript label Apr 3, 2015
@mhegazy mhegazy added the In Discussion Not yet reached consensus label Dec 10, 2015
@RyanCavanaugh
Copy link
Member

@rbuckton can you boil this down to something non-decorator-experts can understand? 😕

@DanielRosenwasser
Copy link
Member

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.

@WanderWang
Copy link

did this issue have any update ? I have the same problem about this

@nicholasguyett
Copy link

@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.

@RyanCavanaugh
Copy link
Member

@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!

@Strate
Copy link

Strate commented Jan 30, 2017

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
}

@sberan
Copy link

sberan commented Jul 17, 2017

@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.

@mhegazy mhegazy added the Domain: Decorators The issue relates to the decorator syntax label Nov 20, 2017
@cntech
Copy link

cntech commented Feb 10, 2018

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.

@danielearwicker
Copy link
Author

@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.

@cntech
Copy link

cntech commented Feb 10, 2018

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.

@danielearwicker
Copy link
Author

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"

@Ixonal
Copy link

Ixonal commented Feb 13, 2018

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";
}

@huafu
Copy link

huafu commented Apr 16, 2018

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

@waterplea
Copy link

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.

@artembatura
Copy link

artembatura commented Jan 24, 2019

@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
image
Bad
image

@christopher-kiss
Copy link

@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.

@infloop
Copy link

infloop commented Mar 11, 2020

Any update on that?

@jcalz
Copy link
Contributor

jcalz commented Feb 12, 2021

Cross-referencing to #39903

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: Decorators The issue relates to the decorator syntax In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests