-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Suggestion for improving generator and async function type checking #6686
Comments
TL;DR: Generator functions and async functions always return instances of intrinsic types, so they can never return instances of other class types. This observation could be used to improve consistency and correctness of return type checking for both generator functions (currently over-loose) and async functions (currently over-strict). |
I asked this question before, and i do not think any thing has changed since #6068 How is the compiler expected to emit this: async function af1(): any {} if as |
@mhegazy I just checked the emit for ES6, so now regardless of the return type annotation the emit is: function asyncFunction() {
return __awaiter(this, void 0, void 0, function* () {/* async function body */});
} and the return new (P || (P = Promise))(function (resolve, reject) {
... So basically for ES6, |
so what is the scenario we are trying to do accomplish here? you can not have a function marked as |
The value is in consistency in the type system. Annotations would ideally work consistently right across the language. I can do all of the following: function f(): any[] { return [1,2,3]; } // OK
function p(): PromiseLike<any> { return Promise.resolve(42); } // OK
function* g(): Iterator<any> { yield 1; yield 2; yield 3; } // OK Note that none of these return type annotations is an exact match to what is actually returned, they are simply assignment-compatible. So now I have my nice consistent mental model, and I try out the following expecting it to work in the same way as the examples above: async function a(): PromiseLike<any> { return 42; } // ERROR ...but it in fact raises a compiler error. The inconsistency is confusing (#5911 raised this very example). And as of #6631, there's no longer a need to keep this inconsistency. |
@yortus suggested I copy my comment from #6631: The fact that the return type must be strictly equal to Promise might become a hindrance in scenarios involving interfaces and/or methods overriding. You can work around it by adding a layer of indirection (like all CS problems 😉) but it doesn't feel nice. Here's one example: in frameworks such as Aurelia, many methods can return a value or a Promise for a value. Not mandating a Promise is both convenient when implementing trivial methods (like Say I have a base class that exposes such a function and is meant to be overriden. Assume that the base default implementation is async. The natural way is this: class BaseViewModel {
async isValid() : boolean | Promise<boolean> {
/* some async implementation, maybe server call */
return await fetch("/valid");
}
}
class DerivedViewModel extends BaseViewModel {
isValid() : boolean | Promise<boolean> {
return true; // Non-async implementation
}
} but this wouldn't work :( |
@jods4 This goes back to the issue that an The only proposal i see here is making a generator function returning something other than the generator interface an error to match the Promise implementation. |
@mhegazy @jods4's example is valid both in terms of its type assertions and its runtime behaviour.
The following workaround adds pointless boilerplate that is only neded to satisfy TypeScript (pointless since @jods4's version works fine at runtime as is currently is written, just removing the annotations) (actually it's much worse than @jods4's original because it creates a new closure instance on every call): class BaseViewModel {
isValid() : boolean | Promise<boolean> {
return (async () => {
/* some async implementation, maybe server call */
})();
}
}
class DerivedViewModel extends BaseViewModel {
isValid() : boolean | Promise<boolean> {
return true; // Non-async implementation
}
} Another workaround just to satisfy the compiler is this (which is also worse than @jods4's original because it instantiates a new implementation of class BaseViewModel {
isValid: () => boolean | Promise<boolean> = async () => {
/* some async implementation, maybe server call */
};
} |
Then please look differently :) Using annotations the way @jods4 does is a great example of the Liskov substitution principle. This surely is part of any good programmer's toolkit. I'd much rather allow LSP to work on async functions, than deliberately stopping it from working on generator functions (where it currently works just fine).
It is categorically not a lie to describe a function foo(): string|number { return 42; } That's guaranteed to return a These are also not lies and may actually be useful annotations for keeping interfaces separate from implementations: var p: PromiseLike<number> = Promise.resolve(42);
var q: Thenable = Promise.reject(new Error('fail!')); The compiler allows these useful 'abstracting' annotations basically everywhere except on async function return types. |
I hate to raise this issue from the dead, but I just ran into this issue while trying to write types for the class ChildProcessPromise extends Promise<ChildProcess> {
public childProcess: ChildProcess;
} I'm unable to return a import { ChildProcessPromise, spawn } from 'child-process-promise';
async function ls(): ChildProcessPromise {
const dir = await getDir();
const promise = spawn('ls', [dir]);
promise.childProcess.stdout.on('data', (data) => process.stdout.write('[spawn] stdout: ', data.toString()));
return promise;
} This produces an error:
It's impossible for the return type to be anything other than a |
@nwalters512 for ES6+ targets, async functions will never return anything other than the built-in This is the expected behaviour of async functions. They work fine with promise-like values in their body, but their return value is always a built-in Promise. |
Ah! I had this idea that returning a promise would bypass the usual wrapping of the return value in a promise, not sure where that came from. Thanks so much. Making the return type |
The return value will not be a |
This is a suggestion to improve both the consistency and the type-safety of return type checking for
generator functions (GFs) and async functions (AFs).
NB: this issue refers to
tsc
behaviour for target >=ES6 withtypescript@next
as of 2016-01-28 (ie since commit a6af98e)Current Behaviour
Consider the following examples of current behaviour for checking return type annotations:
Problems with Current Behaviour
Firstly, type checking is not consistent across the two kinds of functions. In the examples the GF checking is too loose and the AF checking is too strict. The inconsistency is due to the different approach to checking the return type. The two approaches may be summarised like this:
IterableIterator<T>
.Promise<T>
. This is a recent change, the rationalefor it can be followed from here.
Secondly, the type checker only gets 2 out of 4 of the above checks right (
gf1
andaf2
). Explanation:gf1
's return type annotation is not super helpful but is 100% consistent with the type system. No sense erroring here, so the implementation is good.gf2
's return type annotation passes type checking because it passes the assignability check.However
gf2
definitely does not return an instance ofMyIterator
. All generator functionsreturn a generator object, so at runtime
gf2() instanceof MyIterator
isfalse
. A compile error would have been helpful.af1
's return type annotation is just likegf1
: not super helpful but 100% consistent with the type system. The compiler errors here even though nothing is wrong (reason for the error is here).af2
's return type annotation fails type checking because it's notPromise<T>
. The return type definitely won't be an instance of any class other thanPromise
, so the implementation is good.Suggested Improvement
Since GFs and AFs always return instances of known instrinsic types, we can rule out any type annotation that asserts they will return an instance of some other class.
Both generator and async functions could therefore be checked with the same two steps:
Promise<T>
which is allowed for AFs). For example if the return type annotation isFoo
, ensure it does not refer toclass Foo {...}
or another class-like value.These rules have the following effects:
gf2
by ruling out class types likeMyIterator
in addition to checking assignability. GF type checking is made safer in general by catching a class of errors that currently slip through.af1
, because it's no longer necessary to rule out all annotations other thanPromise<T>
, but just those that are assignment-compatible class types likeMyPromise
. This approach will catch the breaking change from 1.7 as a compile error (as desired for reason here), but allow harmless (and correct) things likeany
andPromiseLike<T>
.Working Implementation
This is a small code change. I implemented this in a branch as best I could (but I may have made errors since I'm still getting my head around the codebase). The diff can be seen here.
With this version of
tsc
the above code works as follows:The text was updated successfully, but these errors were encountered: