Suggestion for improving generator and async function type checking
See original GitHub issueThis 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 with typescript@next
as of 2016-01-28 (ie since commit https://github.com/Microsoft/TypeScript/commit/a6af98e10025492e0afda315ea37cfebe0b2bbfb)
Current Behaviour
Consider the following examples of current behaviour for checking return type annotations:
// Current behaviour - generator functions
class MyIterator implements Iterator<any> {next}
function* gf1(): any {} // OK
function* gf2(): MyIterator {} // OK (but really is an error)
// Current behaviour - async functions
class MyPromise extends Promise<any> {}
async function af1(): any {} // ERROR (but is really not an error)
async function af2(): MyPromise {} // ERROR
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:
- Generator functions: accept any return type annotation that is assignable from
IterableIterator<T>
. - Async functions: reject all return type annotations other than references to the global
Promise<T>
. This is a recent change, the rationale for it can be followed from here.
Secondly, the type checker only gets 2 out of 4 of the above checks right (gf1
and af2
). Explanation:
- GOOD:
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. - BAD:
gf2
’s return type annotation passes type checking because it passes the assignability check. Howevergf2
definitely does not return an instance ofMyIterator
. All generator functions return a generator object, so at runtimegf2() instanceof MyIterator
isfalse
. A compile error would have been helpful. - BAD:
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). - GOOD:
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:
- Is Assignable: Ensure the return type of the GF/AF is assignable to the annotated return type. This is the basic check for all kinds of function return types. If not assignable, type checking fails. Otherwise, continue to step 2.
- Not a Class Type: Ensure the return type annotation is not a class type (except
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:
- GF and AF type checking are mutually consistent.
- This fixes
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. - This fixes
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:
// Suggested behaviour - generator functions
class MyIterator implements Iterator<any> {next}
function* gf1(): any {} // OK
function* gf2(): MyIterator {} // ERROR: A generator cannot have a return type annotation of a class type.
// Suggested behaviour - async functions
class MyPromise extends Promise<any> {}
async function af1(): any {} // OK
async function af2(): MyPromise {} // ERROR: An async function cannot have a return type annotation of a class type other than Promise<T>.
Issue Analytics
- State:
- Created 8 years ago
- Reactions:2
- Comments:14 (11 by maintainers)
@nwalters512 for ES6+ targets, async functions will never return anything other than the built-in
Promise
type. You can check it by debugging your own sample code above. If you set a breakpoint on thereturn promise;
line and inspect the value, it has thechildProcess
property as you describe. However if you add code to call this async function and then inspect the result type, it is a standardPromise
and does not have thechildProcess
property.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.
The return value will not be a
Promise<ChildProcessPromise>
despite passing the type checker. A promise never resolves to another promise, by design. ThePromise
instance returned by a call tols
will adopt the state of the specialpromise
instance that was created inside the function. So it will resolve or reject when that inner promise resolves or rejects. But anything else special about that inner promise is lost.