Report error for invalid 'this' type during 'await' (#48946)

This commit is contained in:
Ron Buckton
2022-05-09 13:58:36 -07:00
committed by GitHub
parent f3f0a3f394
commit e2bd89b309
3 changed files with 138 additions and 4 deletions
+33 -4
View File
@@ -36270,7 +36270,7 @@ namespace ts {
* @param type The type of the promise.
* @remarks The "promised type" of a type is the type of the "value" parameter of the "onfulfilled" callback.
*/
function getPromisedTypeOfPromise(type: Type, errorNode?: Node): Type | undefined {
function getPromisedTypeOfPromise(type: Type, errorNode?: Node, thisTypeForErrorOut?: { value?: Type }): Type | undefined {
//
// { // type
// then( // thenFunction
@@ -36312,7 +36312,30 @@ namespace ts {
return undefined;
}
const onfulfilledParameterType = getTypeWithFacts(getUnionType(map(thenSignatures, getTypeOfFirstParameterOfSignature)), TypeFacts.NEUndefinedOrNull);
let thisTypeForError: Type | undefined;
let candidates: Signature[] | undefined;
for (const thenSignature of thenSignatures) {
const thisType = getThisTypeOfSignature(thenSignature);
if (thisType && thisType !== voidType && !isTypeRelatedTo(type, thisType, subtypeRelation)) {
thisTypeForError = thisType;
}
else {
candidates = append(candidates, thenSignature);
}
}
if (!candidates) {
Debug.assertIsDefined(thisTypeForError);
if (thisTypeForErrorOut) {
thisTypeForErrorOut.value = thisTypeForError;
}
if (errorNode) {
error(errorNode, Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1, typeToString(type), typeToString(thisTypeForError));
}
return undefined;
}
const onfulfilledParameterType = getTypeWithFacts(getUnionType(map(candidates, getTypeOfFirstParameterOfSignature)), TypeFacts.NEUndefinedOrNull);
if (isTypeAny(onfulfilledParameterType)) {
return undefined;
}
@@ -36459,7 +36482,8 @@ namespace ts {
return typeAsAwaitable.awaitedTypeOfType = mapType(type, mapper);
}
const promisedType = getPromisedTypeOfPromise(type);
const thisTypeForErrorOut: { value: Type | undefined } = { value: undefined };
const promisedType = getPromisedTypeOfPromise(type, /*errorNode*/ undefined, thisTypeForErrorOut);
if (promisedType) {
if (type.id === promisedType.id || awaitedTypeStack.lastIndexOf(promisedType.id) >= 0) {
// Verify that we don't have a bad actor in the form of a promise whose
@@ -36532,7 +36556,12 @@ namespace ts {
if (isThenableType(type)) {
if (errorNode) {
Debug.assertIsDefined(diagnosticMessage);
error(errorNode, diagnosticMessage, arg0);
let chain: DiagnosticMessageChain | undefined;
if (thisTypeForErrorOut.value) {
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));
}
chain = chainDiagnosticMessages(chain, diagnosticMessage, arg0);
diagnostics.add(createDiagnosticForNodeFromMessageChain(errorNode, chain));
}
return undefined;
}
@@ -0,0 +1,57 @@
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>'.
Type 'number' is not assignable to type 'never'.
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.
The 'this' context of type 'EPromise<number, string>' is not assignable to method's 'this' of type 'EPromise<never, string>'.
==== tests/cases/conformance/async/es2017/await_incorrectThisType.ts (2 errors) ====
// https://github.com/microsoft/TypeScript/issues/47711
type Either<E, A> = Left<E> | Right<A>;
type Left<E> = { tag: 'Left', e: E };
type Right<A> = { tag: 'Right', a: A };
const mkLeft = <E>(e: E): Either<E, never> => ({ tag: 'Left', e });
const mkRight = <A>(a: A): Either<never, A> => ({ tag: 'Right', a });
class EPromise<E, A> implements PromiseLike<A> {
static succeed<A>(a: A): EPromise<never, A> {
return new EPromise(Promise.resolve(mkRight(a)));
}
static fail<E>(e: E): EPromise<E, never> {
return new EPromise(Promise.resolve(mkLeft(e)));
}
constructor(readonly p: PromiseLike<Either<E, A>>) { }
then<B = A, B1 = never>(
// EPromise can act as a Thenable only when `E` is `never`.
this: EPromise<never, A>,
onfulfilled?: ((value: A) => B | PromiseLike<B>) | null | undefined,
onrejected?: ((reason: any) => B1 | PromiseLike<B1>) | null | undefined
): PromiseLike<B | B1> {
return this.p.then(
// Casting to `Right<A>` is safe here because we've eliminated the possibility of `Left<E>`.
either => onfulfilled?.((either as Right<A>).a) ?? (either as Right<A>).a as unknown as B,
onrejected
)
}
}
const withTypedFailure: EPromise<number, string> = EPromise.fail(1);
// Errors as expected:
//
// "The 'this' context of type 'EPromise<number, string>' is not assignable to method's
// 'this' of type 'EPromise<never, string>"
withTypedFailure.then(s => s.toUpperCase()).then(console.log);
~~~~~~~~~~~~~~~~
!!! error TS2684: The 'this' context of type 'EPromise<number, string>' is not assignable to method's 'this' of type 'EPromise<never, string>'.
!!! error TS2684: Type 'number' is not assignable to type 'never'.
async function test() {
await withTypedFailure;
~~~~~~~~~~~~~~~~~~~~~~
!!! error TS1320: Type of 'await' operand must either be a valid promise or must not contain a callable 'then' member.
!!! error TS1320: The 'this' context of type 'EPromise<number, string>' is not assignable to method's 'this' of type 'EPromise<never, string>'.
}
@@ -0,0 +1,48 @@
// @target: esnext
// @noEmit: true
// @noTypesAndSymbols: true
// https://github.com/microsoft/TypeScript/issues/47711
type Either<E, A> = Left<E> | Right<A>;
type Left<E> = { tag: 'Left', e: E };
type Right<A> = { tag: 'Right', a: A };
const mkLeft = <E>(e: E): Either<E, never> => ({ tag: 'Left', e });
const mkRight = <A>(a: A): Either<never, A> => ({ tag: 'Right', a });
class EPromise<E, A> implements PromiseLike<A> {
static succeed<A>(a: A): EPromise<never, A> {
return new EPromise(Promise.resolve(mkRight(a)));
}
static fail<E>(e: E): EPromise<E, never> {
return new EPromise(Promise.resolve(mkLeft(e)));
}
constructor(readonly p: PromiseLike<Either<E, A>>) { }
then<B = A, B1 = never>(
// EPromise can act as a Thenable only when `E` is `never`.
this: EPromise<never, A>,
onfulfilled?: ((value: A) => B | PromiseLike<B>) | null | undefined,
onrejected?: ((reason: any) => B1 | PromiseLike<B1>) | null | undefined
): PromiseLike<B | B1> {
return this.p.then(
// Casting to `Right<A>` is safe here because we've eliminated the possibility of `Left<E>`.
either => onfulfilled?.((either as Right<A>).a) ?? (either as Right<A>).a as unknown as B,
onrejected
)
}
}
const withTypedFailure: EPromise<number, string> = EPromise.fail(1);
// Errors as expected:
//
// "The 'this' context of type 'EPromise<number, string>' is not assignable to method's
// 'this' of type 'EPromise<never, string>"
withTypedFailure.then(s => s.toUpperCase()).then(console.log);
async function test() {
await withTypedFailure;
}