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

Add typedefinitions #209

Closed
wants to merge 3 commits into from
Closed

Conversation

championswimmer
Copy link

Signed-off-by: Arnav Gupta [email protected]

Signed-off-by: Arnav Gupta <[email protected]>
@chriskrycho
Copy link

Possibly useful for comparison: here are mine as a Gist.

@machty
Copy link
Owner

machty commented Feb 27, 2018

I have no TypeScript experience to understand what all of this means/does or how to compare @championswimmer's and @chriskrycho's solutions; could you (and other one else) explain/compare/contrast?

@chriskrycho
Copy link

I'll take a look later this week and compare. Cc. also @dwickern and @mike-north, both of whom have looked at this before.

@championswimmer
Copy link
Author

@chriskrycho
I think I need to change my is[Status] flags to ComputedProperty<boolean> otherwise, my definitions look like they're more exhaustive.

@dwickern
Copy link

dwickern commented Feb 28, 2018

The typings @chriskrycho linked also supports typed arguments to .perform() instead of ...any[]

types/Task.d.ts Outdated
enqueue(): void;
group(groupPath: any): any;
keepLatest(): void;
maxConcurrency(n: number): void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many (all?) of these should be chainable options, and thus cannot return void

export default Controller.extend({
  restartableTask3: task(SHARED_TASK_FN).maxConcurrency(3).restartable(),
  enqueuedTask3:    task(SHARED_TASK_FN).maxConcurrency(3).enqueue(),
  droppingTask3:    task(SHARED_TASK_FN).maxConcurrency(3).drop(),
  keepLatestTask3:  task(SHARED_TASK_FN).maxConcurrency(3).keepLatest(),
});

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah correct, just saw that

types/Task.d.ts Outdated
readonly isSuccessful: boolean;

/** Describes the state that the task instance is in. Can be used for debugging, or potentially driving some UI state. */
readonly state: 'dropped' | 'cancelled' | 'finished' | 'running' | 'waiting';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth breaking this value type out into its own alias, for reuse in libraries/apps that consume ember-concurrency.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good !

types/Task.d.ts Outdated

finally(): RSVP.Promise<any>;

then(): RSVP.Promise<any>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a TaskInstance should be usable as a promise-like thing. Shouldn't then/catch/finally take function arguments, and be generic over the types those callbacks resolve to?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a type for Promise.ThenFunction and Promise.CatchFunction. will use those

@mike-north
Copy link
Contributor

@machty and co. - at risk of stating the obvious, misaligned type information runs the risk of providing a poor dev experience (even worse than the complete absence of type information).

At present, anything involving generator functions is a bit tricky due to gaps in TS (this is the main thing we're waiting on -- as I understand it the underlying type system can handle this already, it's just a debate over the right syntax)

Even after iterating on this, we may want to cut a pre-release and get a few people consuming this before considering a stable release.

@championswimmer
Copy link
Author

@mike-north @chriskrycho @machty
Can you please review this once again

@chriskrycho
Copy link

I'll take a look later; and in particular I will drop it in in our app and make sure it covers all the cases my own typings linked above cover, and if not, suggest how to make sure it does!

Thanks for working hard on this!

@championswimmer
Copy link
Author

Any updates 🙈

@chriskrycho
Copy link

Yes, sorry; ended up very swamped this week.

I've taken some time with this and will block out an hour to dig in on Monday. I can say from reading it carefully that we do need to pull in some of the things my typings have, because they give you completion and type checking for perform invocations and a few other things – but I can also say that your work especially on having docs will be invaluable as we work to get everything tightened down and merged!

@chriskrycho
Copy link

@championswimmer I'm super sorry this keeps getting delayed. We're trying to ship some stuff at work and have hit roadblock after roadblock basically since I got back from EmberConf. 😭 I have a handful of things I'm blocking out time for today, and this is one of them. I'll probably open a PR against your PR to hit the couple pieces I see are missing. You've got a fair number of things I missed, and vice versa!

@chriskrycho
Copy link

And we ran into more derailing today. I will get to this; I apologize for the many delays.

@championswimmer
Copy link
Author

Haha, no problem @chriskrycho :)

@perlun
Copy link

perlun commented Apr 6, 2018

Ping @chriskrycho, we will all be incredibly grateful once this lands. 😇

@machty
Copy link
Owner

machty commented Apr 6, 2018

(i have no idea what I'm talking about) is this something that we can just merge and tweak/improve over time? or is there a lot of damage if we publish something not 100% perfect?

@chriskrycho
Copy link

chriskrycho commented Apr 6, 2018 via email

@championswimmer
Copy link
Author

@chriskrycho got any time this week 🙈

@chriskrycho
Copy link

chriskrycho commented Apr 23, 2018 via email

@josemarluedke
Copy link

Any news on this? I would love to get this as well.

@chriskrycho
Copy link

As my friends back in North Carloina might have said: “Lord wiling and the creek don’t rise…” I’ll get to this on Tuesday.

@championswimmer
Copy link
Author

@chriskrycho
If you could point me towards those types that can improve, I can spend some time on this, this week and get this done with

@lifeart
Copy link

lifeart commented Jul 7, 2018

UP

@mike-north
Copy link
Contributor

mike-north commented Jul 7, 2018

TLDR

I feel pretty strongly that a decorator(s) should be considered as part of an MVP for the first stable ember-concurrency release that contains type information.


I've just spent about a week focusing on Ember+TS ergonomics, and type definitions for ember-concurrency have become one of the "sore thumbs" that stick out (only because so much excellent work has been put into other areas).

Currently (with types much like those in this PR) the way you use e-c with TypeScript is like is like

// This pattern is equivalent to typical use of ember-concurrency
export default class XFoo extends Component.extend({
   // stuff that MUST go on the prototype goes up here
   doSearch: task(function*(this: XFoo) {
      const title = this.get('title'); 
       ...
   }) 
}) {
  public title = '';
  public description = '';
  // vast majority of other interesting stuff goes down here.
}

One challenge (trap?) teams are facing is that there's a "cleaner looking" pattern that's 100% wrong, and the lions share of those new to ES6 classes and TypeScript seem to want to use it

//// THE PATTERN BELOW IS WRONG AND SHOULD NOT BE USED ////

// TS (or ES6 w/ class fields) version
export default class XFoo extends Component {
  public title = '';
  public description = '';
  public doSearch = task(function*(this: XFoo) {
      const title = this.get('title'); 
       ...
  });
}

// ES6 version
export default class XFoo extends Component {
  constructor() {
     super();
     this.doSearch = task(function*(this: XFoo) {
       const title = this.get('title'); 
        ...
      });
  }
}

At its root, there are several factors that allow teams to fall into this trap, including

  • The temptation to incorrectly think of class extends Component { /* stuff */} as equivalent to Component.extend({ /* stuff */}
  • The widespread lack of understanding of how prototypal inheritance works, in general
  • Ember's productivity-increasing "guardrails" result in developers not needing to deeply understand JavaScript objects in order to build an app

I feel pretty strongly that a decorator(s) should be considered as part of an MVP for the first stable ember-concurrency release that contains type information. Consumers don't have to use it, they should be there, and the recommended method of consumption. The goal of this decision would be to guide developers toward ensuring their e-c tasks are defined on prototypes and not each instance.

Decorator(s) could live in a separate addon (i.e., live in ember-decorators), but the release(s) should be coordinated such that ES6/TS consumers are encouraged to use them (or at least nearly 100% of consumers in this group should know the approach is available and less error-prone).

Example usage might look like

import Component from '@ember/component' 
import { task, restartable, maxConcurrency } from 'ember-decorators/task';

export default class XFoo extends Component {
  public title = '';
  public description = '';

  @maxConcurrency(3)
  @restartable
  @task
  public doSearch*() {
      const title = this.get('title'); 
       ...
  }
}

@championswimmer
Copy link
Author

championswimmer commented Jul 7, 2018

@mike-north
This is so very true.
90% of all newcomers to Ember will most definitely treat extend Component similar to Component.extend({}), and they would have no clue what is going wrong.

Though to be fair to the community, there are many instances, like VueJS where the creators have actively put in hours of effort (both in the JS implementation and in creating the required typedefs) to make extends Vue and Vue.extend({}) work exactly similarly with perfect type completion for this.[...] objects using ThisType<T>

@chriskrycho
Copy link

I have been exploring approaches in this space (not just sitting on it, though I've been quiet) for the last several weeks, and I broadly concur with @mike-north's take here. In particular, this –

One challenge (trap?) teams are facing is that there's a "cleaner looking" pattern that's 100% wrong, and the lions share of those new to ES6 classes and TypeScript seem to want to use it

– is the kind of thing that is easy to get wrong even if you do grok the model. I missed it, and just in the last week I had an Ember Core person note that they had missed it as well when I first recommended doing property assignment.

Chris Garrett and I have both been actively documenting this as I can, and I've got a blog post coming Monday or Tuesday which I'll publicize as widely as possible to support it.


The other thing we need to sort out here is how we want to approach typing these. My own typings are more robust than the typings in this pull request, but my experience is that TS's inference ends up falling over with them around half the time – it just doesn't do well with closure arguments at this point. I'll have more to say on this next week, but given the many limitations in TS with generator typings at present, I'm currently inclined toward a more conservative correct-but-not-complete approach, which is I think the best we can actually do here until TS lands proper support for generators.

@mike-north
Copy link
Contributor

mike-north commented Jul 8, 2018

I dug into this even more, and unfortunately have bad news x2

1. My "right way to do e-c w/ TypeScript" is even worse than stated before.

export default class XFoo extends Component.extend({
   // stuff that MUST go on the prototype goes up here
   doSearch: task(function*(this: XFoo) { // << PROBLEM
      const title = this.get('title'); 
       ...
   }) 
}) {
  public title = '';
  public description = '';
  // vast majority of other interesting stuff goes down here.
}

The example above is not valid because we're forbidden from referring to the XFoo type while defining the type. Thus, anything inside the Component.extend({ }) (including the generator passed into task()) must have its lexical scope set up automatically and correctly. The this: XFoo trick is not going to work.

This, however, does work -- the task ends up on the prototype and with the correct type.

export default class XFoo extends Component {
  public doSearch!: Task;
  public title = '';
  public description = '';
}
XFoo.prototype.doSearch =  task(function*(this: XFoo) { // << PROBLEM
   const title = this.get('title'); 
      ...
});

Maybe @chriskrycho's more robust e-c types solve the generator function lexical scope issue, and will save us from having to do this kind of fugly stuff? 🙏🙏🙏🙏🙏

2. TS method/attribute decorator types do not support altering types

see: microsoft/TypeScript#4881 - a discussion about class decorators that covers similar concerns

Essentially, if I have something like this

export default class Foo extends Controller {
  // normal class body definition here
  @task
  public *countdown() {
    yield timeout(1000);
  }
  
  @action
  public startCountdown() {
      this.get('countdown').perform(); // 💥
   }
}

There's no supported way to tell TypeScript that we've altered the shape of the Foo class. countdown will continue to be regarded as just a generator function, and we'll get a type error when we try to access the perform() method.

JS decorators in their current (WIP) state are flexible enough that they can do what needs to be done to create the nice syntax above, but there's a fundamental limitation that would stop us from accurately describing this decorator in ambient type information.

@chriskrycho
Copy link

Unfortunately, the scope issue is what led me to experiment with class fields in the first place. 😞 There’s no workaround for it in the traditional Ember Object world that gets you the relevant types without running into exactly what you found here. The only other thing I’ve thought of (but haven’t tried) is defining the generator function outside the class, including its this argument, and binding it in the prototype. I suspect that will just show the same type error, though.

@gossi
Copy link

gossi commented Jan 5, 2019

Let's warm up this topic. Today, we have decorators for tasks 🎉

Also I did created my own typings, too. Because I find existing ones just afterwards, they are here: https://github.com/gossi/spomoda/blob/master/client/types/ember-concurrency/index.d.ts

What makes me curious is the way to import a task.

  • You did: import { Task } from 'ember-concurrency';
  • While I was going with: import Task from 'ember-concurrency/task'; (felt natural to me)
  • Actually is: import { Task } from 'ember-concurrency/-task-property'; (looks very bad!)

As this might be another aspect (since the code still looks totally different) and might have an impact on it (when it may be will refactored ... to TS?).

@mfeckie
Copy link

mfeckie commented Jan 14, 2019

Awaiting this PR with excitement. As it is so far, I don't think any the proposed solutions cover encapsulated tasks.

For example

foo = task({ 
  count: 1, 
  *perform() {
    this.incrementProperty('count');
    // do other stuff
  } 
})

@buschtoens
Copy link
Contributor

I just had an idea and would love your opinions: machty/ember-concurrency-decorators#30

@buschtoens
Copy link
Contributor

I opened #319 to have another go at this.

@maxfierke
Copy link
Collaborator

Thanks so much for your work on this! However, we're closing this in favor of #357, which has been merged and will be part of 1.2.0

@maxfierke maxfierke closed this Jun 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.