-
-
Notifications
You must be signed in to change notification settings - Fork 157
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 definition based on API docs #357
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lgtm. Idk if I'll have time to test. But having something to iterate on is better than not having anything / each of us copy pastin d.ts everywhere
This lgtm to I'm behind on the TypeScript discussion / tradeoffs when it comes to e-c so I'll defer to others. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, with the caveat about the commented out TaskGroup stuff indeed being public. But that's the only blocker for me.
@maxfierke I have added (mostly just copied) the docs for |
@chancancode I think this looks great! Before I approve, are there any caveats you can think of that would be worth mentioning or potentially breaking for folks (outside of people using their own types)? I'm thinking specifically about TypeScript not supporting changes to the return types of generators at one point, IIRC. |
These PR adds the following top level type symbols:
These can be imported from the Because they are type symbols, non-TypeScript projects won't be able to import them, and TypeScript projects won't be able to do anything with them other than using them as types (e.g. you cannot do These names are effectively "reserved". It would be okay if, for example, ember-concurrency wants to export the same, currently internal, In practice, I think this seems unlikely to be a problem since I was quite careful to pick the names to match the defacto public names in the documentation (
I think this is referring to the fact that the decorator cannot change the type of the property it's decorating. So since most people write This is not really an ember-concurrency problem since the decorators are outside of this repository. Based on the way I typed it, the classic style The workaround in our project (very similar to @NullVoxPopuli's) is the following utility functions: // app/types/ember-concurrency.d.ts
import {
Task,
TaskFunction,
TaskFunctionArgs,
TaskFunctionReturnType,
TaskInstance
} from 'ember-concurrency';
/**
* No-op typecast function that turns what TypeScript believes to be a
* generator function into a Task.
*
* ```js
* import { taskFor } from 'direwolf/types/ember-concurrency';
*
* class Foo extends EmberObject {
* @task *myTask() {
* // ...
* }
*
* someMethod() {
* this.myTask.perform(); // TypeError
* taskFor(this.myTask).perform(); // ok!
* }
* }
* ```
*
* @param task The task. Note that this is purely a typecast function,
* it does not in affect accept a task generator function as input.
*/
export function taskFor<T extends TaskFunction<any, any[]>>(task: T):
Task<TaskFunctionReturnType<T>, TaskFunctionArgs<T>>; // app/types/ember-concurrency.js
import { assert } from '@ember/debug';
export function taskFor(task) {
assert(
`${task} does not appear to be a task!`,
task && typeof task.perform === 'function'
);
return task;
} In actual usage: // @ts-check
// app/components/foo.js
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { task } from 'ember-concurrency-decorators';
import { taskFor } from 'my-app/types/ember-concurrency';
export default class extends Component {
/**
* @param {string} foo
*/
@task *myTask(foo) {
// ...
return 1;
}
async someMethod() {
this.myTask.perform("foo"); // error
this.myTask.isRunning; // error
taskFor(this.myTask).perform("foo"); // ok
taskFor(this.myTask).perform(); // error
taskFor(this.myTask).perform(123); // error
let foo: number = await taskFor(this.myTask).perform("foo"); // ok
let bar: string = await taskFor(this.myTask).perform("foo"); // error
taskFor(this.myTask).isRunning; // ok
let task = taskFor(this.myTask);
task.perform("foo"); // ok
task.perform(); // error
task.perform(123); // error
foo = await task.perform("foo"); // ok
bar = await task.perform("foo"); // error
task.isRunning; // ok
}
} |
Once machty/ember-concurrency#357 has been merged/released
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chancancode thanks! that all makes a lot of sense.
Two questions:
- would you be willing to add a page to the docs w/ that example?
- would it make sense to include such a type-cast function as part of this?
Sure, but I'll need a couple of pointers:
I'm not sure. For one, it's kind of weird to have an empty function that doesn't do anything for non-TypeScript users. The other is that it's basically an "unsafe" function that works by lying to the compiler to mask the type error. I don't think it's particular dangerous, because at the end of the day it's just a weird quirk/fallout, but I'm not sure it's good to include it as an official API. |
Yep, you can do this in HBS: {{! BEGIN-SNIPPET some-template-name }}
{{! END-SNIPPET }} or in JS/TS // BEGIN-SNIPPET
// END-SNIPPET It uses https://github.com/ef4/ember-code-snippet underneath, so anything mentioned in the docs should apply within e-c (though, we use 2.4.x, so we do have syntax highlighting and do not have the
Yep, it should.
Anecdotally, it seems most folks using TypeScript are using it with decorators, so we can probably stick to those.
I'm assuming both are fairly similar? Glimmer would probably be okay on its own, but if there are notable differences, it might be good to note those.
Probably worth mentioning in the docs, but we should probably prefer using regular ES getters.
|
I'm writing some docs, but sounds like @chriskrycho wants to review this also |
awesome! his suggestion of type tests would be super valuable too, if that's a thing |
addon/index.d.ts
Outdated
@@ -0,0 +1,828 @@ | |||
import ComputedProperty from '@ember/object/computed'; | |||
|
|||
export type TaskGenerator<T> = Generator<any, T, unknown>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export type TaskGenerator<T> = Generator<any, T, unknown>; | |
export type TaskGenerator<T> = Generator<any, T, any>; |
Should the third parameter (the LHS of yield ...
, i.e. what it "returns") be unknown
or any
?
Due to TS limitations, there is no way we can type that properly. What the yield
"returns" is based on what you yielded, and that is just not possible to type. await
has a similar problem but is hardcoded to work in the TS compiler.
The first parameter (the RHS of the yield ...
, i.e. what you are allowed to yield to ember-concurrency) is typed to any
because there is no real utility to limit it, it really can be anything (including nothing – you may just be yielding so ember-concurrency can pause to check for isDestroying
and such).
The third parameter is typed to unknown
, but I am not sure if that is too strict. In practice, it will force every consumer to check the value. If you are in the mood for it you may write an assertion:
class Foo {
@task *myTask() {
let foo /* unknown */ = yield someStringPromise;
assert('foo should have resolved into a string!', typeof foo === 'string');
// now foo is a string
}
}
...that's fine when it's easy enough to check, but it's this is really checking for bugs in the library than in your code, and you would never have written that assertion there if not for TS. For more complex objects you may end up needing to write elaborate type assertion functions that are otherwise not needed. In practice, a lot of people is just going to end up writing this anyway, every single time they yield in a task:
class Foo {
@task *myTask() {
let foo: MyType = (yield someStringPromise) as any;
}
}
If that's what everyone is going to do at the end of the day, it just seems a little silly not to type it as any
in the first place. In practice, so long as you know the type of the RHS, there is very little reason not to trust e-c to call you with the correct thing on the LHS. If you are worried that you would have mistyped the LHS, you could do this:
type Resolved<T> = T extends PromiseLike<infer R> ? R : T;
class Foo {
@task *myTask() {
let foo: Resolved<typeof someStringPromise> = yield someStringPromise;
}
}
That seems like a pretty good compromise between correctness and verbosity, and led me to thinking we should go with any
instead of unknown
here. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I realized my mistake (I actually thought about it when I typed this), in practice it doesn't really matter what we typed here, because the types are never really used for inference in the position I was refereeing to above. Since it's inside the body of a generator function, TypeScript is just going to do whatever it normally does to generator functions without ever considering the type we have here. This type is only used when assigning a generator function to task(...)
, etc, as long as the type is wide enough that TS allows assigning any valid task function to positions where we take one as a TaskFunction<T>
, it doesn't really matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm on the same page given the constraints TS gives us. I would recommend we very carefully document this, however, because it's the kind of thing that could easily bite people by letting any
leak in when they're unaware of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wrong again. Here is how it comes up in practice (typing the return type of the generator function) and the ergonomic differences:
This doesn't look so bad since the types are just strings and the assertion is easy to write, but basically see my first comment for the tradeoffs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Talked to @chriskrycho on Discord, it seems like we are leaning towards using any
here and documenting/recommending the Resolved
type trick
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, the main question is do we want to force the consumer to do a check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you noted, it's a very frustrating experience to have to when the type is actually knowable and local. I don't have a strong opinion here (and I normally very strongly favor unknown
and forcing the check).
cc. also @dfreeman @jamescdavis
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'll just go ahead and write a compiler plugin that transforms @task async
methods into generators and no have to worry about this 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went with any
in the end and documented it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great set of types, and I don't have any objections to the types themselves as they stand! However, I'd like to very strongly recommend that we add a set of type tests, of the same sort I recently added to ember-modifier. The short of that:
- Install the
expect-type
library - Create a type test file somewhere in the library; as long as it can import the add-on's types it should be good to go. (This may require adding a
tsconfig.json
file next to it to support it.) - Write type assertions that make sure that the public API of the module is what it should be—the
expectTypeOf
type assertion will prevent accidentally widening toany
, for example, if you give it a specific type. - Add an ember-try config that runs against a supported set of TypeScript versions—see ember-modifier's ember-try.js and ember-try-typescript.js for a good example.
- Related to the ember-try config: define the supported TypeScript versions. We recommend supporting 3.6, 3.7, and 3.9 at present (all the versions released during the current or previous stable Ember LTS, excluding 3.8 because it had serious problems with Ember's types)
(I'm actually in the final stages of polishing up a Typed Ember RFC with guidance recommending exactly this strategy for all addons which ship types. Obviously that won't be able to force anyone's hand, any more than we can force anything for addons in the ecosystem in general, but we're hoping to get everyone on the same page so that we can have a set of solid guarantees around stable types in the ecosystem. I'll link that here when it's done, hopefully by the end of the day today.)
As promised, Typed Ember RFC: Type Stability for Addons. |
<h3>Using ember-concurrency with TypeScript</h3> | ||
|
||
<p> | ||
As of version 1.2, ember-concurrency comes bundled with its own |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assumed we are going to bump minor version for this.
Note that old versions of <a href="https://github.com/machty/ember-concurrency-decorators/"> | ||
ember-concurrency-decorators</a> came with an incomplete type | ||
definition for ember-concurrency. If you are using that addon, | ||
be sure to upgrade to the latest version to avoid conflicts. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assumed this as well. I'm not sure what the version number would be, so I just said "latest". There is a 1.1 "alpha" that is pretty unrelated, so we may want to skip 1.1 to avoid confusion. Jumping to 1.2 would also match up with the e-c version number, a happy accident.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(still relevant)
Pushed a new commit to add the type tests, ember-try and CI config |
Thank you so much! I have this on my list for the week—it will likely be Tuesday or Wednesday before I have time to review it in detail! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definite 👍 from me, but will defer merging until @chriskrycho is able to take another look. The docs look excellent!
This matches the upcoming support matrix in machty/ember-concurrency#357 This drops TS 3.5 as a tested configuration. This is inevitable, since we will start depending on the types from upstream instead of vendoring our own, and the upstream types does not support 3.5. in comparasion even more breaking than this change, so it shouldn't be an issue to drop 3.5 here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had a handful of questions and suggestions. I will review the type tests tomorrow, but wanted to keep the conversation moving!
export interface EncapsulatedTaskDescriptor<T, Args extends any[]> { | ||
perform(...args: Args): TaskGenerator<T>; | ||
} | ||
|
||
export type EncapsulatedTaskDescriptorArgs<T extends EncapsulatedTaskDescriptor<any, any[]>> = | ||
T extends { perform(...args: infer A): TaskGenerator<any> } ? A : []; | ||
|
||
export type EncapsulatedTaskDescriptorReturnType<T extends EncapsulatedTaskDescriptor<any, any[]>> = | ||
T extends { perform(...args: any[]): TaskGenerator<infer R> } ? R : unknown; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the benefit of naming these Encapsulated*
rather than just TaskDescriptor
etc.? Particular in terms of the imports we're exposing with them, TaskDescriptor
, TaskDescriptorArgs
, and TaskDescriptorReturn
all seem much better—and I don't see any overload with anything else here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the feature is called and documented as EncapsulatedTask
. I considered just calling it EncapsulatedTask
, but that's not quite right, because this is the thing you pass in (similar to the POJO you pass to EmberObject.extend(...)
) to create a encapsulated task (technically, an encapsulated task property, which gets turned into an encapsulated task, which can be instantiated as encapsulated task instances). It just so happens that the current implementation of the task
function normalizes away the "encapsulated-ness" of it so there aren't EncapsulatedTaskProperty
, EncapsulatedTask
and EncapsulatedTaskInstance
exports, but there very well may be in the future.
As far as TaskDescriptor
, that also sounds like too generic of a term, as it seems feasible that there will be a TaskDescriptor
that is unrelated to EncapsulatedTask
.
Ultimately, I question whether some of these APIs are still needed/still a good fit in the post-Octane world, but ultimately, it just doesn't seem like my decision to make or my job to speculate on the future direction too much in this PR, as my goal is just to document what exists today.
So EncapsulatedTaskDescriptorArgs
seems like the most conservative, and accurate, if a bit verbose, option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense; thanks for the explanation! I had forgotten about the feature in question. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really great work on these tests! I caught a number of things, but this is delightfully exhaustive and a fantastic model for the community. Thank you!
}, | ||
"exclude":[ | ||
"node_modules", | ||
"node_modules/ember-concurrency-decorators/types/ember-concurrency.d.ts", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting—why is this needed? Is it not covered by the previous exclude, or is it just here to be extra explicit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not 100% sure that it did anything, tbh. However, e-c-d ships with its own ambient (not imported by anything) declaration that conflicts with the definitions here. When importing from e-c-d, it seems to cause that file to be "picked up" and causes type errors in the language server in VS Code, which appears to be fixed by this, or that I got lucky after adding it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In any case, I'll try to remember to come back and remove this once e-c-d has been updated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's do this! Thanks again so much for the massive lift here!
Yet another attempt at adding types.
Quite similar to #355, but this includes every item (and only those items) in the public API documentation. Also uses interfaces over classes, since the e-c classes are not meant to be instantiated manually or subclassed by apps.