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 Type Definitions #319

Closed
wants to merge 1 commit into from
Closed

Conversation

buschtoens
Copy link
Contributor

@buschtoens buschtoens commented Sep 18, 2019

This copies the types from machty/ember-concurrency-decorators#56 and together with that PR will finally enable type-safe usage of ember-concurrency.

Closes #209, though we should compare both.

Not 💯 sure what's left to do here and whether / how to test this.

/cc @jamescdavis @chriskrycho @dfreeman @josemarluedke

@chriskrycho
Copy link

I at least can give this a close look on Friday (have time allocated for miscellaneous open source woke then!).

*/
cancel(): void;

catch(): RSVP.Promise<unknown>;

Choose a reason for hiding this comment

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

Are there any downsides to using the native Promise here?

Suggested change
catch(): RSVP.Promise<unknown>;
catch(): Promise<unknown>;

Choose a reason for hiding this comment

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

Yes: it’s not what the library actually returns! RSVP is a compatible superset type-wise, so you can test it as a normal Promise in your own codebase as a consumer, but if you need to take advantage of its being RSVP, you can’t if you make this change.

Copy link
Contributor

@mike-north mike-north left a comment

Choose a reason for hiding this comment

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

I haven't had a chance to look at this thoroughly, but a good start would be to add a good number of test cases w/ dtslint.

import {
UnwrapComputedPropertyGetter,
UnwrapComputedPropertyGetters
} from '@ember/object/-private/types';
Copy link
Contributor

Choose a reason for hiding this comment

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

These private types change all the time, and are often in need of adjustment to respond to breaking TS compiler changes. Please do not consume these directly

Copy link
Contributor

Choose a reason for hiding this comment

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

Strongly agree.

* Describes the state that the task instance is in.
* Can be used for debugging, or potentially driving some UI state.
*/
export enum TaskInstanceState {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd avoid creating the TaskInstance and TaskState enums, since they suggest that values (not just types) can be imported from ember-concurrency. If it's necessary to make some utility types available to consumers, that should probably be done through a ghost module to avoid clashing with anything that might be exported as a value from the underlying library in the future

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

haha. I suspect the quality bar may be different different between Chris' personal gist and merging something in as official typings.

Choose a reason for hiding this comment

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

That’s just crazy talk. 😆 I agree here. Much as the enums are nice, if they can’t actually be imported we shouldn’t use them. Instead, we should just use literal types.

/**
* Retrieves the value of a property from the object.
*/
get<K extends keyof this>(key: K): UnwrapComputedPropertyGetter<this[K]>;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is going to break for cases like this.get('foo.bar'). Not all values that can be passed into this function are keyof this

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we just say getting in this way is not supported when using TS?

This is the current state of the types, given that we don't have the ability to use unknown as long as we continue to support TS 2.8.

Something like this (available in TS 3.0+) would support Ember's public API fully, without compromising type safety

interface Getter {
  get<K extends keyof this>(key: K): this[K];
  get(key: string): unknown;
}

interface Foo extends Getter {
  blue: string[];
  red: {
    green: string;
  }
}

const foo: Foo = {} as any;

foo.get('blue'); // string[];
foo.get('red.green'); // unknown

Copy link

@chriskrycho chriskrycho Sep 20, 2019

Choose a reason for hiding this comment

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

We should note that as a point for discussion Soon(tm). 3.0 was a long time ago and the official support policy we decided on currently only commits us to 3.5 and 3.6, I believe. We’ve intentionally supported a wider range, but it’s been quite some time and it’s worth revisiting what we support (outside this PR!).

Copy link
Contributor

@jamescdavis jamescdavis left a comment

Choose a reason for hiding this comment

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

This is looking really good. I just have a few comments/questions.

getProperties<K extends keyof this>(
...list: K[]
): Pick<UnwrapComputedPropertyGetters<this>, K>;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the goal here to prevent (via types) setting values on tasks and task instances? Maybe we should just extend EmberObject (since tasks and task instances are)?

export function waitForQueue(queueName: string): Promise<void>;

export function task<Args extends any[], R>(
taskFn: GeneratorFn<unknown, Args, R>
Copy link
Contributor

Choose a reason for hiding this comment

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

Typing the task function is awesome for type safety, but, unfortunately, breaks "classic" tasks that need this:

export default class MyComponent extends Component.extend({
    myClassicTask: task(function *(this: MyComponent) {
        if (this.foo) {
            
    })
}) {
    foo = true;
    
}

Results in: tsserver: (2506) 'MyComponent' is referenced directly or indirectly in its own base expression.

The typedefs currently in ember-osf-web sacrifice type safety to make this work (which I'm not happy with, of course).

There is a workaround, which is to use an interface:

interface IMyComponent {
    foo: boolean;
}

export default class MyComponent extends Component.extend({
    myClassicTask: task(function *(this: IMyComponent) {
        if (this.foo) {
            
    })
}) {
    foo = true;
    
}

which works, but is not super great. Of course the real solution is to move these tasks into the class body and use ember-concurrency-decorators, but for us (at least) we were hoping to be able to do that incrementally. I still think what you have here is correct, we just might need to communicate the caveat that it will break "classic" tasks and suggest the workaround (or another better one, if it exists).

Copy link
Contributor

Choose a reason for hiding this comment

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

^ this is some great feedback, and another good reason to let tests (both for classic and octane use cases) be your guide

Copy link
Contributor

Choose a reason for hiding this comment

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

we just might need to communicate the caveat that it will break "classic" tasks and suggest the workaround (or another better one, if it exists).

I'm not sure I agree with this, given that vscode uses type information now even for apps that do not have type-checking enabled. One of the immediate wins of landing this would be having in-editor documentation/autocomplete around the Task, TaskInstance, TaskGroup APIs. If anything, "classic" usage may be significantly more important to cater to than octane usage at this time.

I still feel that we should cover both as part of any initial release of type information to ember-concurrency. See details in the previous PR here: #209 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, but also don't know how we'll make this work. As soon as you type taskFn as a callable (even () => any) you get the error about referencing the class in its own base expression, because, well, you are. When it's typed as just any (what we've been doing in our app) I'm guessing the compiler just ignores it outside the function. There may be a way to type this so that it will work, but I'm not seeing it.

/**
* Retrieves the value of a property from the object.
*/
get<K extends keyof this>(key: K): UnwrapComputedPropertyGetter<this[K]>;
Copy link
Contributor

Choose a reason for hiding this comment

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

import {
UnwrapComputedPropertyGetter,
UnwrapComputedPropertyGetters
} from '@ember/object/-private/types';
Copy link
Contributor

Choose a reason for hiding this comment

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

Strongly agree.

* `TaskInstances`, and at any point, you can call the `.cancelAll()` method
* on this object to cancel all running or enqueued `TaskInstance`s.
*/
export interface Task<Args extends any[], T> extends Getter {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this extend TaskProperty? Otherwise, we don't get the modifiers.

| InstanceRunning
| InstanceWaiting
| InstanceError
| InstanceSuccess);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is cool because it guarantees valid state properties, but it also seems to break .get(). For:

const taskInstance = this.myTask.perform();
taskInstance.get('isRunning');

I get:

This expression is not callable.
    Each member of the union type '(<K extends "error" | "get" | "getProperties" | "hasStarted" | "isCanceled" | "isDropped" | "isFinished" | "isRunning" | "isSuccessful" | "value" | "cancel" | "catch" | "finally" | "then" | "state">(key: K) => UnwrapComputedPropertyGetter<...>) | ... 4 more ... | (<K extends "error" | ... 13 more ... | "state">(key:...' has signatures, but none of those signatures are compatible with each other.

Choose a reason for hiding this comment

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

It’s worth seeing if this works if we drop the magic bits imported for get—I haven’t had a chance to test, but I think it may, between that and extending from EmberObject? Because at that point it should be actually the same underlying type definition being invoked.

Copy link
Contributor

Choose a reason for hiding this comment

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

Extending from EmberObject results in the same error. Removing the union of state interfaces works and also intersection with just one of the state interfaces works. Intersecting with the union of two or more of the states breaks. It looks like UnwrapComputedPropertyGetter can't handle unions of interfaces with different signatures, which makes sense given that it uses infer.

* Describes the state that the task instance is in.
* Can be used for debugging, or potentially driving some UI state.
*/
export enum TaskInstanceState {
Copy link
Contributor

Choose a reason for hiding this comment

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


export function taskGroup(): TaskGroupProperty;

interface CommonTaskProperty {

Choose a reason for hiding this comment

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

should this also have .withTestWaiter? or is it recommended that people use the wrapping version?

@NullVoxPopuli
Copy link

NullVoxPopuli commented Apr 2, 2020

I think this method should go somewhere:

import Task from 'ember-concurrency/task';

type ECTask<Args extends Array<any>, Return> = (
  ...args: Args
) => Generator<any /* potentially yielded types */, Return, unknown>;

export function taskFor<Args extends any[], Return = void>(generatorFn: ECTask<Args, Return>) {
  return (generatorFn as any) as Task<Args, Return>;
}

image

This is more in conjunction with ember-concurrency-decorators though

image

This only required that I change the perform definition to:

    perform(...args: PerformArgs): TaskInstance<PerformReturn>;

(and then passed the supporting generics around)

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

7 participants