-
-
Notifications
You must be signed in to change notification settings - Fork 25
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
TypeScript : "Property 'perform' does not exist..." on Task #30
Comments
That warning is "correct". Unfortunately decorators still cannot change the type signature of the thing that they decorate. The TypeScript will only further develop decorators support, once the proposal for decorators hit stage 3. But they are aware and have it on their road map. I have gone through numerous ideas for workarounds in the #e-typescript and they all fall flat on their face in one they or another. Another big problem is that the type definition for generator functions loses some information, as the types of all The "best" solution would require significant syntactical overhead to force-feed TypeScript: import Component from '@ember/component';
import { task, Task } from 'ember-concurrency-decorators';
export default class ExampleComponent extends Component {
@task
*doStuff() {
}
executeTheTask() {
((this.doStuff as unknown) as Task).perform();
}
} The above code assumes a |
Unfortunately it is currently impossible to extract the return value of a generator function: microsoft/TypeScript#2983 But I have another idea to make this addon somewhat more type-safe: import Controller from '@ember/controller';
import { task, taskGroup, TaskGroup, Task, asTask } from 'ember-concurrency-decorators';
export class Foo extends Controller {
@taskGroup saveTaskGroup!: TaskGroup;
@task
*someTask(someArg: number) {
yield 'foo';
return false;
}
@task
encapsulated = {
*perform(someArg: number) {
yield 'foo';
return false;
}
};
exec() {
const foo = asTask(this.someTask).perform<boolean>(3);
const bar = asTask(this.encapsulated).perform<boolean>(3);
this.saveTaskGroup.cancelAll();
}
} Some explanation is due: Since task groups don't get assigned a value, we can just add a type and non-null assertion operator to promise TypeScript, that this property is gonna be set. @taskGroup saveTaskGroup!: TaskGroup; This allows plain and simple access without any further trickery like you would normally do: this.saveTaskGroup.cancelAll(); For tasks it is not as pretty. I took some inspiration from const foo = asTask(this.someTask).perform<boolean>(3); // => Promise<boolean>
const bar = asTask(this.encapsulated).perform<boolean>(3); // => Promise<boolean>
Unfortunately, we cannot infer the return type of a generator function (microsoft/TypeScript#2983). Even more unfortunately we also cannot specify the return type as a generic type parameter to the const foo = asTask<boolean>(this.someTask).perform(3);
const bar = asTask<boolean>(this.encapsulated).perform(3); This would break the type inferring for the arguments, because of microsoft/TypeScript#26349. 🙈 I have the type side of things working on a branch and will experiment a bit with it, and see whether I can optimize this further somehow. Writing the babel transform should be fairly easy. Apropos, why do we need the babel transform in the first place? If const foo = this.someTask.perform(3);
const bar = this.encapsulated.perform(3); |
If we assume, that users only If I add the following (unfinished) types: export function asTask<Args extends any[], R>(
task: GeneratorFn<Args, R>
): Task<Args, Exclude<R, Promise<any>>>;
export function asTask<Args extends any[], R>(task: {
perform: GeneratorFn<Args, R>;
}): Task<Args, Exclude<R, Promise<any>>>;
export interface Task<Args extends any[], R> {
perform(...args: Args): Promise<R>;
lastSuccessful?: {
value: R;
};
// ...
}
export interface TaskGroup {
cancelAll(): void;
// ...
}
type GeneratorFn<Args extends any[] = any[], R = any> = (
...args: Args
) => IterableIterator<R>; The following works: import Controller from '@ember/controller';
import { task, taskGroup, TaskGroup, Task, asTask } from 'ember-concurrency-decorators';
export class Foo extends Controller {
@taskGroup saveTaskGroup!: TaskGroup;
@task
*someTask(someArg: number) {
yield Promise.resolve('foo');
return false;
}
@task
encapsulated = {
*perform(someArg: number) {
yield Promise.resolve('foo');
return false;
}
};
exec() {
const foo = asTask(this.someTask).perform(3); // => Promise<false>
const bar = asTask(this.encapsulated).perform(3); // => Promise<false>
this.saveTaskGroup.cancelAll();
}
} Big but: if users yield a non- class {
@task
*someTask(someArg: number) {
yield Promise.resolve('foo');
yield 'bar';
return false;
}
exec() {
const foo = asTask(this.someTask).perform(3); // => Promise<'bar' | false>
}
} IMO yielding a something that is not a In worst case scenario, they could still cast the return value without |
@jamescdavis suggested another great idea on Discord (thread). He wrote types that allow the following: class Foo {
myTask = task(function*(this: Foo) {
yield Promise.resolve('foo');
return 'bar';
});
exec() {
this.myTask.perform();
}
} AFAICT this has a problem though. It should cause machty/ember-concurrency#271. But if you combine this with the babel transform approach, we could transform all occurrences of The massive advantage of this approach is, that you don't have to wrap your task in On the long run, when TS finally supports the new decorators spec, we'll probably want to converge on the I think I can easily implement both solutions in parallel. We can also provide codemods to switch between the two and also to strip |
First of all I want to say I commend everybody's effort in making this addon in an attempt to bridge some gaps in the ecosystem right now. But to be honest, this issue makes this addon a bit useless. As it stands, you can only use this addon if you are using ES6 classes with JavaScript. I would say people are mostly adopting native classes because of TypeScript - which this addon essentially doesn't support right now (in no small part due to TypeScript itself). Coupled with the fact that the main addon doesn't work at all using native-classes, this leaves a pretty decent amount of projects that essentially cannot use For what it's worth, you can get-by for now by just laundering your types through init() {
super.init();
this.myTask.perform();
}
// @NOTE Launder types here by explicitly typing `myTask` as `any`
@task
myTask: any = function*() {
// ...
}; |
Thanks 😊
I'm totally with you!
Yes, I do think so! I haven't tested this, but I think this would be the right direction. import { timeout } from 'ember-concurrency';
import { task } from 'ember-concurrency-decorators';
type GeneratorFn<Args extends any[] = any[], R = any> = (
...args: Args
) => IterableIterator<R>;
function asTask<Args extends any[], R>(
task: GeneratorFn<Args, R>
): Task<Args, Exclude<R, Promise<any>>> {
// @ts-ignore
return task;
}
interface Task<Args extends any[], R> {
perform(...args: Args): Promise<R>;
lastSuccessful?: {
value: R;
};
// ...
}
class Foo {
bar = 1337;
@task
myTask = asTask(function*(this: Foo) {
yield timeout(500);
return this.bar;
});
} |
I made a PoC in #50. |
This is now solved by https://github.com/chancancode/ember-concurrency-ts https://github.com/chancancode/ember-concurrency-ts#alternate-usage-of-taskfor provides the same functionality as #56 |
The following causes typescript to raise the type error at the bottom:
The text was updated successfully, but these errors were encountered: