Skip to content

Commit e2bd89b

Browse files
authored
Report error for invalid 'this' type during 'await' (#48946)
1 parent f3f0a3f commit e2bd89b

File tree

3 files changed

+138
-4
lines changed

3 files changed

+138
-4
lines changed

src/compiler/checker.ts

+33-4
Original file line numberDiff line numberDiff line change
@@ -36270,7 +36270,7 @@ namespace ts {
3627036270
* @param type The type of the promise.
3627136271
* @remarks The "promised type" of a type is the type of the "value" parameter of the "onfulfilled" callback.
3627236272
*/
36273-
function getPromisedTypeOfPromise(type: Type, errorNode?: Node): Type | undefined {
36273+
function getPromisedTypeOfPromise(type: Type, errorNode?: Node, thisTypeForErrorOut?: { value?: Type }): Type | undefined {
3627436274
//
3627536275
// { // type
3627636276
// then( // thenFunction
@@ -36312,7 +36312,30 @@ namespace ts {
3631236312
return undefined;
3631336313
}
3631436314

36315-
const onfulfilledParameterType = getTypeWithFacts(getUnionType(map(thenSignatures, getTypeOfFirstParameterOfSignature)), TypeFacts.NEUndefinedOrNull);
36315+
let thisTypeForError: Type | undefined;
36316+
let candidates: Signature[] | undefined;
36317+
for (const thenSignature of thenSignatures) {
36318+
const thisType = getThisTypeOfSignature(thenSignature);
36319+
if (thisType && thisType !== voidType && !isTypeRelatedTo(type, thisType, subtypeRelation)) {
36320+
thisTypeForError = thisType;
36321+
}
36322+
else {
36323+
candidates = append(candidates, thenSignature);
36324+
}
36325+
}
36326+
36327+
if (!candidates) {
36328+
Debug.assertIsDefined(thisTypeForError);
36329+
if (thisTypeForErrorOut) {
36330+
thisTypeForErrorOut.value = thisTypeForError;
36331+
}
36332+
if (errorNode) {
36333+
error(errorNode, Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1, typeToString(type), typeToString(thisTypeForError));
36334+
}
36335+
return undefined;
36336+
}
36337+
36338+
const onfulfilledParameterType = getTypeWithFacts(getUnionType(map(candidates, getTypeOfFirstParameterOfSignature)), TypeFacts.NEUndefinedOrNull);
3631636339
if (isTypeAny(onfulfilledParameterType)) {
3631736340
return undefined;
3631836341
}
@@ -36459,7 +36482,8 @@ namespace ts {
3645936482
return typeAsAwaitable.awaitedTypeOfType = mapType(type, mapper);
3646036483
}
3646136484

36462-
const promisedType = getPromisedTypeOfPromise(type);
36485+
const thisTypeForErrorOut: { value: Type | undefined } = { value: undefined };
36486+
const promisedType = getPromisedTypeOfPromise(type, /*errorNode*/ undefined, thisTypeForErrorOut);
3646336487
if (promisedType) {
3646436488
if (type.id === promisedType.id || awaitedTypeStack.lastIndexOf(promisedType.id) >= 0) {
3646536489
// Verify that we don't have a bad actor in the form of a promise whose
@@ -36532,7 +36556,12 @@ namespace ts {
3653236556
if (isThenableType(type)) {
3653336557
if (errorNode) {
3653436558
Debug.assertIsDefined(diagnosticMessage);
36535-
error(errorNode, diagnosticMessage, arg0);
36559+
let chain: DiagnosticMessageChain | undefined;
36560+
if (thisTypeForErrorOut.value) {
36561+
chain = chainDiagnosticMessages(chain, Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1, typeToString(type), typeToString(thisTypeForErrorOut.value));
36562+
}
36563+
chain = chainDiagnosticMessages(chain, diagnosticMessage, arg0);
36564+
diagnostics.add(createDiagnosticForNodeFromMessageChain(errorNode, chain));
3653636565
}
3653736566
return undefined;
3653836567
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
tests/cases/conformance/async/es2017/await_incorrectThisType.ts(40,1): error TS2684: The 'this' context of type 'EPromise<number, string>' is not assignable to method's 'this' of type 'EPromise<never, string>'.
2+
Type 'number' is not assignable to type 'never'.
3+
tests/cases/conformance/async/es2017/await_incorrectThisType.ts(43,5): error TS1320: Type of 'await' operand must either be a valid promise or must not contain a callable 'then' member.
4+
The 'this' context of type 'EPromise<number, string>' is not assignable to method's 'this' of type 'EPromise<never, string>'.
5+
6+
7+
==== tests/cases/conformance/async/es2017/await_incorrectThisType.ts (2 errors) ====
8+
// https://github.com/microsoft/TypeScript/issues/47711
9+
type Either<E, A> = Left<E> | Right<A>;
10+
type Left<E> = { tag: 'Left', e: E };
11+
type Right<A> = { tag: 'Right', a: A };
12+
13+
const mkLeft = <E>(e: E): Either<E, never> => ({ tag: 'Left', e });
14+
const mkRight = <A>(a: A): Either<never, A> => ({ tag: 'Right', a });
15+
16+
class EPromise<E, A> implements PromiseLike<A> {
17+
static succeed<A>(a: A): EPromise<never, A> {
18+
return new EPromise(Promise.resolve(mkRight(a)));
19+
}
20+
21+
static fail<E>(e: E): EPromise<E, never> {
22+
return new EPromise(Promise.resolve(mkLeft(e)));
23+
}
24+
25+
constructor(readonly p: PromiseLike<Either<E, A>>) { }
26+
27+
then<B = A, B1 = never>(
28+
// EPromise can act as a Thenable only when `E` is `never`.
29+
this: EPromise<never, A>,
30+
onfulfilled?: ((value: A) => B | PromiseLike<B>) | null | undefined,
31+
onrejected?: ((reason: any) => B1 | PromiseLike<B1>) | null | undefined
32+
): PromiseLike<B | B1> {
33+
return this.p.then(
34+
// Casting to `Right<A>` is safe here because we've eliminated the possibility of `Left<E>`.
35+
either => onfulfilled?.((either as Right<A>).a) ?? (either as Right<A>).a as unknown as B,
36+
onrejected
37+
)
38+
}
39+
}
40+
41+
const withTypedFailure: EPromise<number, string> = EPromise.fail(1);
42+
43+
// Errors as expected:
44+
//
45+
// "The 'this' context of type 'EPromise<number, string>' is not assignable to method's
46+
// 'this' of type 'EPromise<never, string>"
47+
withTypedFailure.then(s => s.toUpperCase()).then(console.log);
48+
~~~~~~~~~~~~~~~~
49+
!!! error TS2684: The 'this' context of type 'EPromise<number, string>' is not assignable to method's 'this' of type 'EPromise<never, string>'.
50+
!!! error TS2684: Type 'number' is not assignable to type 'never'.
51+
52+
async function test() {
53+
await withTypedFailure;
54+
~~~~~~~~~~~~~~~~~~~~~~
55+
!!! error TS1320: Type of 'await' operand must either be a valid promise or must not contain a callable 'then' member.
56+
!!! error TS1320: The 'this' context of type 'EPromise<number, string>' is not assignable to method's 'this' of type 'EPromise<never, string>'.
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// @target: esnext
2+
// @noEmit: true
3+
// @noTypesAndSymbols: true
4+
5+
// https://github.com/microsoft/TypeScript/issues/47711
6+
type Either<E, A> = Left<E> | Right<A>;
7+
type Left<E> = { tag: 'Left', e: E };
8+
type Right<A> = { tag: 'Right', a: A };
9+
10+
const mkLeft = <E>(e: E): Either<E, never> => ({ tag: 'Left', e });
11+
const mkRight = <A>(a: A): Either<never, A> => ({ tag: 'Right', a });
12+
13+
class EPromise<E, A> implements PromiseLike<A> {
14+
static succeed<A>(a: A): EPromise<never, A> {
15+
return new EPromise(Promise.resolve(mkRight(a)));
16+
}
17+
18+
static fail<E>(e: E): EPromise<E, never> {
19+
return new EPromise(Promise.resolve(mkLeft(e)));
20+
}
21+
22+
constructor(readonly p: PromiseLike<Either<E, A>>) { }
23+
24+
then<B = A, B1 = never>(
25+
// EPromise can act as a Thenable only when `E` is `never`.
26+
this: EPromise<never, A>,
27+
onfulfilled?: ((value: A) => B | PromiseLike<B>) | null | undefined,
28+
onrejected?: ((reason: any) => B1 | PromiseLike<B1>) | null | undefined
29+
): PromiseLike<B | B1> {
30+
return this.p.then(
31+
// Casting to `Right<A>` is safe here because we've eliminated the possibility of `Left<E>`.
32+
either => onfulfilled?.((either as Right<A>).a) ?? (either as Right<A>).a as unknown as B,
33+
onrejected
34+
)
35+
}
36+
}
37+
38+
const withTypedFailure: EPromise<number, string> = EPromise.fail(1);
39+
40+
// Errors as expected:
41+
//
42+
// "The 'this' context of type 'EPromise<number, string>' is not assignable to method's
43+
// 'this' of type 'EPromise<never, string>"
44+
withTypedFailure.then(s => s.toUpperCase()).then(console.log);
45+
46+
async function test() {
47+
await withTypedFailure;
48+
}

0 commit comments

Comments
 (0)