Skip to content

Commit c4876d5

Browse files
committed
Add support for awaiting union types with mixed promise and non-promise constituents.
1 parent 2891a1d commit c4876d5

File tree

5 files changed

+214
-73
lines changed

5 files changed

+214
-73
lines changed

src/compiler/checker.ts

Lines changed: 93 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -7653,7 +7653,7 @@ namespace ts {
76537653
// Promise/A+ compatible implementation will always assimilate any foreign promise, so the
76547654
// return type of the body should be unwrapped to its awaited type, which we will wrap in
76557655
// the native Promise<T> type later in this function.
7656-
type = getAwaitedType(type, func, Diagnostics.Return_expression_in_async_function_does_not_have_a_valid_callable_then_member);
7656+
type = checkAwaitedType(type, func, Diagnostics.Return_expression_in_async_function_does_not_have_a_valid_callable_then_member);
76577657
}
76587658
}
76597659
else {
@@ -7762,7 +7762,7 @@ namespace ts {
77627762
// Promise/A+ compatible implementation will always assimilate any foreign promise, so the
77637763
// return type of the body should be unwrapped to its awaited type, which should be wrapped in
77647764
// the native Promise<T> type by the caller.
7765-
type = getAwaitedType(type, body.parent, Diagnostics.Return_expression_in_async_function_does_not_have_a_valid_callable_then_member);
7765+
type = checkAwaitedType(type, body.parent, Diagnostics.Return_expression_in_async_function_does_not_have_a_valid_callable_then_member);
77667766
}
77677767

77687768
if (!contains(aggregatedTypes, type)) {
@@ -7914,7 +7914,7 @@ namespace ts {
79147914
let exprType = checkExpression(<Expression>node.body);
79157915
if (returnType) {
79167916
if (isAsync) {
7917-
let awaitedType = getAwaitedType(exprType, node.body, Diagnostics.Expression_body_for_async_arrow_function_does_not_have_a_valid_callable_then_member);
7917+
let awaitedType = checkAwaitedType(exprType, node.body, Diagnostics.Expression_body_for_async_arrow_function_does_not_have_a_valid_callable_then_member);
79187918
checkTypeAssignableTo(awaitedType, promisedType, node.body);
79197919
}
79207920
else {
@@ -8041,7 +8041,7 @@ namespace ts {
80418041
}
80428042

80438043
let operandType = checkExpression(node.expression);
8044-
return getAwaitedType(operandType, node);
8044+
return checkAwaitedType(operandType, node);
80458045
}
80468046

80478047
function checkPrefixUnaryExpression(node: PrefixUnaryExpression): Type {
@@ -9474,10 +9474,10 @@ namespace ts {
94749474
error(location, message);
94759475
}
94769476

9477-
return false;
9477+
return unknownType;
94789478
}
94799479

9480-
return true;
9480+
return type;
94819481
}
94829482

94839483
/**
@@ -9537,7 +9537,7 @@ namespace ts {
95379537
return getTypeAtPosition(signature, 0);
95389538
}
95399539

9540-
let alreadySeenTypesForAwait: boolean[] = [];
9540+
let awaitedTypeStack: number[] = [];
95419541

95429542
/**
95439543
* Gets the "awaited type" of a type.
@@ -9546,76 +9546,96 @@ namespace ts {
95469546
* Promise-like type; otherwise, it is the type of the expression. This is used to reflect
95479547
* The runtime behavior of the `await` keyword.
95489548
*/
9549-
function getAwaitedType(type: Type, location?: Node, message?: DiagnosticMessage): Type {
9550-
// reset the set of visited types
9551-
alreadySeenTypesForAwait.length = 0;
9552-
while (true) {
9553-
let promisedType = getPromisedType(type);
9554-
if (promisedType === undefined) {
9555-
// The type was not a PromiseLike, so it could not be unwrapped any further.
9556-
// As long as the type does not have a callable "then" property, then it is
9557-
// safe to return the type; otherwise, an error will have been reported in
9558-
// the call to checkNonThenableType and we will return unknownType.
9559-
//
9560-
// An example of a non-promise "thenable" might be:
9561-
//
9562-
// await { then(): void {} }
9563-
//
9564-
// The "thenable" does not match the minimal definition for a PromiseLike. When
9565-
// a Promise/A+-compatible or ES6 promise tries to adopt this value, the promise
9566-
// will never settle. We treat this as an error to help flag an early indicator
9567-
// of a runtime problem. If the user wants to return this value from an async
9568-
// function, they would need to wrap it in some other value. If they want it to
9569-
// be treated as a promise, they can cast to <any>.
9570-
if (!checkNonThenableType(type, location, message)) {
9571-
type = unknownType;
9549+
function getAwaitedType(type: Type) {
9550+
return checkAwaitedType(type, /*location*/ undefined, /*message*/ undefined);
9551+
}
9552+
9553+
function checkAwaitedType(type: Type, location?: Node, message?: DiagnosticMessage) {
9554+
return getAwaitedTypeWorker(type);
9555+
9556+
function getAwaitedTypeWorker(type: Type): Type {
9557+
if (type.flags & TypeFlags.Union) {
9558+
let types: Type[] = [];
9559+
for (let constituentType of (<UnionType>type).types) {
9560+
types.push(getAwaitedTypeWorker(constituentType));
95729561
}
95739562

9574-
break;
9563+
return getUnionType(types);
95759564
}
9576-
9577-
// Keep track of the type we're about to unwrap to avoid bad recursive promise types.
9578-
// See the comments below for more information.
9579-
alreadySeenTypesForAwait[type.id] = true;
9580-
9581-
if (alreadySeenTypesForAwait[promisedType.id]) {
9582-
// We have a bad actor in the form of a promise whose promised type is the same
9583-
// promise type, or a mutually recursive promise. Return the unknown type as we cannot guess
9584-
// the shape. If this were the actual case in the JavaScript, this Promise would never resolve.
9585-
//
9586-
// An example of a bad actor with a singly-recursive promise type might be:
9587-
//
9588-
// interface BadPromise {
9589-
// then(onfulfilled: (value: BadPromise) => any, onrejected: (error: any) => any): BadPromise;
9590-
// }
9591-
//
9592-
// The above interface will pass the PromiseLike check, and return a promised type of `BadPromise`.
9593-
// Since this is a self reference, we don't want to keep recursing ad infinitum.
9594-
//
9595-
// An example of a bad actor in the form of a mutually-recursive promise type might be:
9596-
//
9597-
// interface BadPromiseA {
9598-
// then(onfulfilled: (value: BadPromiseB) => any, onrejected: (error: any) => any): BadPromiseB;
9599-
// }
9600-
//
9601-
// interface BadPromiseB {
9602-
// then(onfulfilled: (value: BadPromiseA) => any, onrejected: (error: any) => any): BadPromiseA;
9603-
// }
9604-
//
9605-
if (location) {
9606-
error(location, Diagnostics._0_is_referenced_directly_or_indirectly_in_the_fulfillment_callback_of_its_own_then_method, symbolToString(type.symbol));
9565+
else {
9566+
let promisedType = getPromisedType(type);
9567+
if (promisedType === undefined) {
9568+
// The type was not a PromiseLike, so it could not be unwrapped any further.
9569+
// As long as the type does not have a callable "then" property, it is
9570+
// safe to return the type; otherwise, an error will have been reported in
9571+
// the call to checkNonThenableType and we will return unknownType.
9572+
//
9573+
// An example of a non-promise "thenable" might be:
9574+
//
9575+
// await { then(): void {} }
9576+
//
9577+
// The "thenable" does not match the minimal definition for a PromiseLike. When
9578+
// a Promise/A+-compatible or ES6 promise tries to adopt this value, the promise
9579+
// will never settle. We treat this as an error to help flag an early indicator
9580+
// of a runtime problem. If the user wants to return this value from an async
9581+
// function, they would need to wrap it in some other value. If they want it to
9582+
// be treated as a promise, they can cast to <any>.
9583+
return checkNonThenableType(type, location, message);
9584+
}
9585+
else {
9586+
if (type.id === promisedType.id || awaitedTypeStack.indexOf(promisedType.id) >= 0) {
9587+
// We have a bad actor in the form of a promise whose promised type is
9588+
// the same promise type, or a mutually recursive promise. Return the
9589+
// unknown type as we cannot guess the shape. If this were the actual
9590+
// case in the JavaScript, this Promise would never resolve.
9591+
//
9592+
// An example of a bad actor with a singly-recursive promise type might
9593+
// be:
9594+
//
9595+
// interface BadPromise {
9596+
// then(
9597+
// onfulfilled: (value: BadPromise) => any,
9598+
// onrejected: (error: any) => any): BadPromise;
9599+
// }
9600+
//
9601+
// The above interface will pass the PromiseLike check, and return a
9602+
// promised type of `BadPromise`. Since this is a self reference, we
9603+
// don't want to keep recursing ad infinitum.
9604+
//
9605+
// An example of a bad actor in the form of a mutually-recursive
9606+
// promise type might be:
9607+
//
9608+
// interface BadPromiseA {
9609+
// then(
9610+
// onfulfilled: (value: BadPromiseB) => any,
9611+
// onrejected: (error: any) => any): BadPromiseB;
9612+
// }
9613+
//
9614+
// interface BadPromiseB {
9615+
// then(
9616+
// onfulfilled: (value: BadPromiseA) => any,
9617+
// onrejected: (error: any) => any): BadPromiseA;
9618+
// }
9619+
//
9620+
if (location) {
9621+
error(
9622+
location,
9623+
Diagnostics._0_is_referenced_directly_or_indirectly_in_the_fulfillment_callback_of_its_own_then_method,
9624+
symbolToString(type.symbol));
9625+
}
9626+
9627+
return unknownType;
9628+
}
9629+
9630+
// Keep track of the type we're about to unwrap to avoid bad recursive promise types.
9631+
// See the comments above for more information.
9632+
awaitedTypeStack.push(type.id);
9633+
let awaitedType = getAwaitedTypeWorker(promisedType);
9634+
awaitedTypeStack.pop();
9635+
return awaitedType;
96079636
}
9608-
9609-
type = unknownType;
9610-
break;
96119637
}
9612-
9613-
type = promisedType;
96149638
}
9615-
9616-
// Cleanup, reset the set of visited types
9617-
alreadySeenTypesForAwait.length = 0;
9618-
return type;
96199639
}
96209640

96219641
/**
@@ -9691,7 +9711,7 @@ namespace ts {
96919711
}
96929712

96939713
// Get and return the awaited type of the return type.
9694-
return getAwaitedType(promiseType, node, Diagnostics.An_async_function_or_method_must_have_a_valid_awaitable_return_type);
9714+
return checkAwaitedType(promiseType, node, Diagnostics.An_async_function_or_method_must_have_a_valid_awaitable_return_type);
96959715
}
96969716

96979717
/** Check a decorator */
@@ -10664,7 +10684,7 @@ namespace ts {
1066410684
else if (func.type || isGetAccessorWithAnnotatatedSetAccessor(func) || signature.typePredicate) {
1066510685
if (isAsyncFunctionLike(func)) {
1066610686
let promisedType = getPromisedType(returnType);
10667-
let awaitedType = getAwaitedType(exprType, node.expression, Diagnostics.Return_expression_in_async_function_does_not_have_a_valid_callable_then_member);
10687+
let awaitedType = checkAwaitedType(exprType, node.expression, Diagnostics.Return_expression_in_async_function_does_not_have_a_valid_callable_then_member);
1066810688
checkTypeAssignableTo(awaitedType, promisedType, node.expression);
1066910689
}
1067010690
else {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//// [awaitUnion_es6.ts]
2+
declare let a: number | string;
3+
declare let b: PromiseLike<number> | PromiseLike<string>;
4+
declare let c: PromiseLike<number | string>;
5+
declare let d: number | PromiseLike<string>;
6+
declare let e: number | PromiseLike<number | string>;
7+
async function f() {
8+
let await_a = await a;
9+
let await_b = await b;
10+
let await_c = await c;
11+
let await_d = await d;
12+
let await_e = await e;
13+
}
14+
15+
//// [awaitUnion_es6.js]
16+
function f() {
17+
return __awaiter([this], function* () {
18+
let await_a = yield a;
19+
let await_b = yield b;
20+
let await_c = yield c;
21+
let await_d = yield d;
22+
let await_e = yield e;
23+
});
24+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
=== tests/cases/conformance/async/es6/awaitUnion_es6.ts ===
2+
declare let a: number | string;
3+
>a : Symbol(a, Decl(awaitUnion_es6.ts, 0, 11))
4+
5+
declare let b: PromiseLike<number> | PromiseLike<string>;
6+
>b : Symbol(b, Decl(awaitUnion_es6.ts, 1, 11))
7+
>PromiseLike : Symbol(PromiseLike, Decl(lib.d.ts, 1187, 163))
8+
>PromiseLike : Symbol(PromiseLike, Decl(lib.d.ts, 1187, 163))
9+
10+
declare let c: PromiseLike<number | string>;
11+
>c : Symbol(c, Decl(awaitUnion_es6.ts, 2, 11))
12+
>PromiseLike : Symbol(PromiseLike, Decl(lib.d.ts, 1187, 163))
13+
14+
declare let d: number | PromiseLike<string>;
15+
>d : Symbol(d, Decl(awaitUnion_es6.ts, 3, 11))
16+
>PromiseLike : Symbol(PromiseLike, Decl(lib.d.ts, 1187, 163))
17+
18+
declare let e: number | PromiseLike<number | string>;
19+
>e : Symbol(e, Decl(awaitUnion_es6.ts, 4, 11))
20+
>PromiseLike : Symbol(PromiseLike, Decl(lib.d.ts, 1187, 163))
21+
22+
async function f() {
23+
>f : Symbol(f, Decl(awaitUnion_es6.ts, 4, 53))
24+
25+
let await_a = await a;
26+
>await_a : Symbol(await_a, Decl(awaitUnion_es6.ts, 6, 4))
27+
28+
let await_b = await b;
29+
>await_b : Symbol(await_b, Decl(awaitUnion_es6.ts, 7, 4))
30+
31+
let await_c = await c;
32+
>await_c : Symbol(await_c, Decl(awaitUnion_es6.ts, 8, 4))
33+
34+
let await_d = await d;
35+
>await_d : Symbol(await_d, Decl(awaitUnion_es6.ts, 9, 4))
36+
37+
let await_e = await e;
38+
>await_e : Symbol(await_e, Decl(awaitUnion_es6.ts, 10, 4))
39+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
=== tests/cases/conformance/async/es6/awaitUnion_es6.ts ===
2+
declare let a: number | string;
3+
>a : string | number
4+
5+
declare let b: PromiseLike<number> | PromiseLike<string>;
6+
>b : PromiseLike<number> | PromiseLike<string>
7+
>PromiseLike : PromiseLike<T>
8+
>PromiseLike : PromiseLike<T>
9+
10+
declare let c: PromiseLike<number | string>;
11+
>c : PromiseLike<string | number>
12+
>PromiseLike : PromiseLike<T>
13+
14+
declare let d: number | PromiseLike<string>;
15+
>d : number | PromiseLike<string>
16+
>PromiseLike : PromiseLike<T>
17+
18+
declare let e: number | PromiseLike<number | string>;
19+
>e : number | PromiseLike<string | number>
20+
>PromiseLike : PromiseLike<T>
21+
22+
async function f() {
23+
>f : () => Promise<void>
24+
25+
let await_a = await a;
26+
>await_a : string | number
27+
>a : any
28+
29+
let await_b = await b;
30+
>await_b : string | number
31+
>b : any
32+
33+
let await_c = await c;
34+
>await_c : string | number
35+
>c : any
36+
37+
let await_d = await d;
38+
>await_d : string | number
39+
>d : any
40+
41+
let await_e = await e;
42+
>await_e : string | number
43+
>e : any
44+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// @target: ES6
2+
// @noEmitHelpers: true
3+
declare let a: number | string;
4+
declare let b: PromiseLike<number> | PromiseLike<string>;
5+
declare let c: PromiseLike<number | string>;
6+
declare let d: number | PromiseLike<string>;
7+
declare let e: number | PromiseLike<number | string>;
8+
async function f() {
9+
let await_a = await a;
10+
let await_b = await b;
11+
let await_c = await c;
12+
let await_d = await d;
13+
let await_e = await e;
14+
}

0 commit comments

Comments
 (0)