Skip to content

Commit 0b6ad03

Browse files
committed
Report error for invalid 'this' type during 'await'
1 parent 9469f95 commit 0b6ad03

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
@@ -36256,7 +36256,7 @@ namespace ts {
3625636256
* @param type The type of the promise.
3625736257
* @remarks The "promised type" of a type is the type of the "value" parameter of the "onfulfilled" callback.
3625836258
*/
36259-
function getPromisedTypeOfPromise(type: Type, errorNode?: Node): Type | undefined {
36259+
function getPromisedTypeOfPromise(type: Type, errorNode?: Node, thisTypeForErrorOut?: { value?: Type }): Type | undefined {
3626036260
//
3626136261
// { // type
3626236262
// then( // thenFunction
@@ -36298,7 +36298,30 @@ namespace ts {
3629836298
return undefined;
3629936299
}
3630036300

36301-
const onfulfilledParameterType = getTypeWithFacts(getUnionType(map(thenSignatures, getTypeOfFirstParameterOfSignature)), TypeFacts.NEUndefinedOrNull);
36301+
let thisTypeForError: Type | undefined;
36302+
let candidates: Signature[] | undefined;
36303+
for (const thenSignature of thenSignatures) {
36304+
const thisType = getThisTypeOfSignature(thenSignature);
36305+
if (thisType && thisType !== voidType && !isTypeRelatedTo(type, thisType, subtypeRelation)) {
36306+
thisTypeForError = thisType;
36307+
}
36308+
else {
36309+
candidates = append(candidates, thenSignature);
36310+
}
36311+
}
36312+
36313+
if (!candidates) {
36314+
Debug.assertIsDefined(thisTypeForError);
36315+
if (thisTypeForErrorOut) {
36316+
thisTypeForErrorOut.value = thisTypeForError;
36317+
}
36318+
if (errorNode) {
36319+
error(errorNode, Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1, typeToString(type), typeToString(thisTypeForError));
36320+
}
36321+
return undefined;
36322+
}
36323+
36324+
const onfulfilledParameterType = getTypeWithFacts(getUnionType(map(candidates, getTypeOfFirstParameterOfSignature)), TypeFacts.NEUndefinedOrNull);
3630236325
if (isTypeAny(onfulfilledParameterType)) {
3630336326
return undefined;
3630436327
}
@@ -36445,7 +36468,8 @@ namespace ts {
3644536468
return typeAsAwaitable.awaitedTypeOfType = mapType(type, mapper);
3644636469
}
3644736470

36448-
const promisedType = getPromisedTypeOfPromise(type);
36471+
const thisTypeForErrorOut: { value: Type | undefined } = { value: undefined };
36472+
const promisedType = getPromisedTypeOfPromise(type, /*errorNode*/ undefined, thisTypeForErrorOut);
3644936473
if (promisedType) {
3645036474
if (type.id === promisedType.id || awaitedTypeStack.lastIndexOf(promisedType.id) >= 0) {
3645136475
// Verify that we don't have a bad actor in the form of a promise whose
@@ -36518,7 +36542,12 @@ namespace ts {
3651836542
if (isThenableType(type)) {
3651936543
if (errorNode) {
3652036544
Debug.assertIsDefined(diagnosticMessage);
36521-
error(errorNode, diagnosticMessage, arg0);
36545+
let chain: DiagnosticMessageChain | undefined;
36546+
if (thisTypeForErrorOut.value) {
36547+
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));
36548+
}
36549+
chain = chainDiagnosticMessages(chain, diagnosticMessage, arg0);
36550+
diagnostics.add(createDiagnosticForNodeFromMessageChain(errorNode, chain));
3652236551
}
3652336552
return undefined;
3652436553
}
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)