From 77875368036faf717ff132eea8289f1c4682cc59 Mon Sep 17 00:00:00 2001 From: Harsh Vardhan Chauhan Date: Mon, 26 Oct 2026 00:59:13 +0530 Subject: [PATCH 1/4] Fix 'this' type in object literal methods returned from async functions The contextual type of an async return expression is T | PromiseLike. Exclude the synthesized PromiseLike when deriving 'this'. --- tsc/internal/checker/checker.go | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 148cf5fde47dd..ddca6009fa23f 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -12288,6 +12288,19 @@ func (c *Checker) getContextualThisParameterType(fn *ast.Node) *Type { // for 'this' is the non-null form of the contextual type for the containing object literal or // the type of the object literal itself. if contextualType != nil { + if contextualType.flags&TypeFlagsUnion != 0 && c.isAsyncReturnExpression(containingLiteral) { + // The contextual type of an async return expression is `T | PromiseLike`; `this` is just `T`. + globalPromiseLike := c.getGlobalPromiseLikeType() + // Only remove a `PromiseLike` that was synthesized for a sibling union member `U`. + members := contextualType.Types() + contextualType = c.filterType(contextualType, func(t *Type) bool { + if t.objectFlags&ObjectFlagsReference == 0 || t.Target() != globalPromiseLike { + return true + } + args := c.getTypeArguments(t) + return len(args) != 1 || !slices.Contains(members, args[0]) + }) + } thisType = c.GetNonNullableType(contextualType) } else { thisType = c.checkExpressionCached(containingLiteral) @@ -12315,6 +12328,28 @@ func (c *Checker) getContextualThisParameterType(fn *ast.Node) *Type { return nil } +func (c *Checker) isAsyncReturnExpression(node *ast.Node) bool { + parent := node.Parent + for parent != nil && ast.IsParenthesizedExpression(parent) { + parent = parent.Parent + } + if parent == nil { + return false + } + var fn *ast.Node + switch { + case ast.IsReturnStatement(parent): + fn = ast.GetContainingFunction(parent) + case ast.IsArrowFunction(parent) && ast.SkipParentheses(parent.Body()) == node: + fn = parent + } + if fn == nil { + return false + } + flags := ast.GetFunctionFlags(fn) + return flags&ast.FunctionFlagsAsync != 0 && flags&ast.FunctionFlagsGenerator == 0 +} + func (c *Checker) checkThisExpression(node *ast.Node) *Type { // Stop at the first arrow function so that we can // tell whether 'this' needs to be captured. From 7afceffa85300e69f498e43ff18d6118e6a3632a Mon Sep 17 00:00:00 2001 From: Harsh Vardhan Chauhan Date: Wed, 28 Oct 2026 03:06:34 +0530 Subject: [PATCH 2/4] Address review: precise synthesized PromiseLike removal, keep thenables, add tests --- tsc/internal/checker/checker.go | 57 +++++--- ...syncReturnObjectLiteralThisType.errors.txt | 31 +++++ .../asyncReturnObjectLiteralThisType.symbols | 98 ++++++++++++++ .../asyncReturnObjectLiteralThisType.types | 123 ++++++++++++++++++ .../asyncReturnObjectLiteralThisType.ts | 21 +++ 5 files changed, 313 insertions(+), 17 deletions(-) create mode 100644 tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.errors.txt create mode 100644 tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.types create mode 100644 tsc/testdata/tests/cases/compiler/asyncReturnObjectLiteralThisType.ts diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 708c6dd6d214d..445d83706f2a6 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -12291,18 +12291,8 @@ func (c *Checker) getContextualThisParameterType(fn *ast.Node) *Type { // for 'this' is the non-null form of the contextual type for the containing object literal or // the type of the object literal itself. if contextualType != nil { - if contextualType.flags&TypeFlagsUnion != 0 && c.isAsyncReturnExpression(containingLiteral) { - // The contextual type of an async return expression is `T | PromiseLike`; `this` is just `T`. - globalPromiseLike := c.getGlobalPromiseLikeType() - // Only remove a `PromiseLike` that was synthesized for a sibling union member `U`. - members := contextualType.Types() - contextualType = c.filterType(contextualType, func(t *Type) bool { - if t.objectFlags&ObjectFlagsReference == 0 || t.Target() != globalPromiseLike { - return true - } - args := c.getTypeArguments(t) - return len(args) != 1 || !slices.Contains(members, args[0]) - }) + if contextualType.flags&TypeFlagsUnion != 0 { + contextualType = c.removeSynthesizedPromiseLike(containingLiteral, contextualType) } thisType = c.GetNonNullableType(contextualType) } else { @@ -12331,26 +12321,59 @@ func (c *Checker) getContextualThisParameterType(fn *ast.Node) *Type { return nil } -func (c *Checker) isAsyncReturnExpression(node *ast.Node) bool { +// The contextual type of an async function's return expression is `T | PromiseLike`, where `T` is the awaited +// contextual return type. For `this` in methods of a returned object literal, the `PromiseLike` member is an +// artifact of that wrapping, so remove exactly that synthesized member. It is kept if the literal has a `then` +// member, since the literal may then be intended as the thenable. +func (c *Checker) removeSynthesizedPromiseLike(literal *ast.Node, contextualType *Type) *Type { + fn := getAsyncFunctionReturningExpression(literal) + if fn == nil { + return contextualType + } + for _, p := range literal.AsObjectLiteralExpression().Properties.Nodes { + if p.Name() != nil { + if name, ok := ast.TryGetTextOfPropertyName(p.Name()); ok && name == "then" { + return contextualType + } + } + } + returnType := c.getContextualReturnType(fn, ContextFlagsNone) + if returnType == nil { + return contextualType + } + awaitedType := c.mapType(returnType, c.getAwaitedTypeNoAlias) + if awaitedType == nil { + return contextualType + } + synthesized := c.createPromiseLikeType(awaitedType) + return c.filterType(contextualType, func(t *Type) bool { return t != synthesized }) +} + +// Returns the async, non-generator function whose return value (a `return` operand or concise arrow body) is node. +func getAsyncFunctionReturningExpression(node *ast.Node) *ast.Node { parent := node.Parent for parent != nil && ast.IsParenthesizedExpression(parent) { + node = parent parent = parent.Parent } if parent == nil { - return false + return nil } var fn *ast.Node switch { case ast.IsReturnStatement(parent): fn = ast.GetContainingFunction(parent) - case ast.IsArrowFunction(parent) && ast.SkipParentheses(parent.Body()) == node: + case ast.IsArrowFunction(parent) && parent.Body() == node: fn = parent } if fn == nil { - return false + return nil } flags := ast.GetFunctionFlags(fn) - return flags&ast.FunctionFlagsAsync != 0 && flags&ast.FunctionFlagsGenerator == 0 + if flags&ast.FunctionFlagsAsync != 0 && flags&ast.FunctionFlagsGenerator == 0 { + return fn + } + return nil } func (c *Checker) checkThisExpression(node *ast.Node) *Type { diff --git a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.errors.txt b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.errors.txt new file mode 100644 index 0000000000000..dea78912b1eaa --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.errors.txt @@ -0,0 +1,31 @@ +asyncReturnObjectLiteralThisType.ts(12,45): error TS1058: The return type of an async function must either be a valid promise or must not contain a callable 'then' member. +asyncReturnObjectLiteralThisType.ts(13,29): error TS2339: Property 'value' does not exist on type 'PromiseLike<{ value: number; }> | { value: number; }'. + Property 'value' does not exist on type 'PromiseLike<{ value: number; }>'. + + +==== asyncReturnObjectLiteralThisType.ts (2 errors) ==== + type X = { value: number; method(): void }; + declare function foo(cb: () => Promise): void; + foo(async () => ({ value: 5, method() { this satisfies X; } })); + foo(async () => { return { value: 5, method() { this satisfies X; } }; }); + foo(async function () { return { value: 5, method() { this satisfies X; } }; }); + async function f(): Promise { return { value: 1, method() { this satisfies X; } }; } + const test2: () => Promise = async () => { return { value: 5, method() { this satisfies X; } }; }; + // nullable awaited + const n1: () => Promise = async () => ({ value: 1, method() { this satisfies X; } }); + const n2: () => Promise = async () => { return { value: 1, method() { this satisfies X; } }; }; + // thenable must not get this: X + const t: () => Promise<{ value: number }> = async () => ({ + ~~~~~~~~~~~~~~ + then(resolve: any) { this.value.toFixed(); }, + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~~~~~ +!!! error TS2339: Property 'value' does not exist on type 'PromiseLike<{ value: number; }> | { value: number; }'. +!!! error TS2339: Property 'value' does not exist on type 'PromiseLike<{ value: number; }>'. + }); + ~~ +!!! error TS1058: The return type of an async function must either be a valid promise or must not contain a callable 'then' member. + // declared PromiseLike remains + declare function g(cb: () => X | PromiseLike): void; + g(async () => ({ value: 1, method() { this satisfies X; } })); + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.symbols b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.symbols new file mode 100644 index 0000000000000..14da480bc5700 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.symbols @@ -0,0 +1,98 @@ +//// [tests/cases/compiler/asyncReturnObjectLiteralThisType.ts] //// + +=== asyncReturnObjectLiteralThisType.ts === +type X = { value: number; method(): void }; +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 0, 10)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 0, 25)) + +declare function foo(cb: () => Promise): void; +>foo : Symbol(foo, Decl(asyncReturnObjectLiteralThisType.ts, 0, 43)) +>cb : Symbol(cb, Decl(asyncReturnObjectLiteralThisType.ts, 1, 21)) +>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2018.promise.d.ts, --, --)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) + +foo(async () => ({ value: 5, method() { this satisfies X; } })); +>foo : Symbol(foo, Decl(asyncReturnObjectLiteralThisType.ts, 0, 43)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 2, 18)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 2, 28)) +>this : Symbol(__type, Decl(asyncReturnObjectLiteralThisType.ts, 0, 8)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) + +foo(async () => { return { value: 5, method() { this satisfies X; } }; }); +>foo : Symbol(foo, Decl(asyncReturnObjectLiteralThisType.ts, 0, 43)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 3, 26)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 3, 36)) +>this : Symbol(__type, Decl(asyncReturnObjectLiteralThisType.ts, 0, 8)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) + +foo(async function () { return { value: 5, method() { this satisfies X; } }; }); +>foo : Symbol(foo, Decl(asyncReturnObjectLiteralThisType.ts, 0, 43)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 4, 32)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 4, 42)) +>this : Symbol(__type, Decl(asyncReturnObjectLiteralThisType.ts, 0, 8)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) + +async function f(): Promise { return { value: 1, method() { this satisfies X; } }; } +>f : Symbol(f, Decl(asyncReturnObjectLiteralThisType.ts, 4, 80)) +>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2018.promise.d.ts, --, --)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 5, 41)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 5, 51)) +>this : Symbol(__type, Decl(asyncReturnObjectLiteralThisType.ts, 0, 8)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) + +const test2: () => Promise = async () => { return { value: 5, method() { this satisfies X; } }; }; +>test2 : Symbol(test2, Decl(asyncReturnObjectLiteralThisType.ts, 6, 5)) +>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2018.promise.d.ts, --, --)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 6, 54)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 6, 64)) +>this : Symbol(__type, Decl(asyncReturnObjectLiteralThisType.ts, 0, 8)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) + +// nullable awaited +const n1: () => Promise = async () => ({ value: 1, method() { this satisfies X; } }); +>n1 : Symbol(n1, Decl(asyncReturnObjectLiteralThisType.ts, 8, 5)) +>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2018.promise.d.ts, --, --)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 8, 50)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 8, 60)) +>this : Symbol(__type, Decl(asyncReturnObjectLiteralThisType.ts, 0, 8)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) + +const n2: () => Promise = async () => { return { value: 1, method() { this satisfies X; } }; }; +>n2 : Symbol(n2, Decl(asyncReturnObjectLiteralThisType.ts, 9, 5)) +>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2018.promise.d.ts, --, --)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 9, 63)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 9, 73)) +>this : Symbol(__type, Decl(asyncReturnObjectLiteralThisType.ts, 0, 8)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) + +// thenable must not get this: X +const t: () => Promise<{ value: number }> = async () => ({ +>t : Symbol(t, Decl(asyncReturnObjectLiteralThisType.ts, 11, 5)) +>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2018.promise.d.ts, --, --)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 11, 24)) + + then(resolve: any) { this.value.toFixed(); }, +>then : Symbol(then, Decl(asyncReturnObjectLiteralThisType.ts, 11, 58)) +>resolve : Symbol(resolve, Decl(asyncReturnObjectLiteralThisType.ts, 12, 7)) + +}); +// declared PromiseLike remains +declare function g(cb: () => X | PromiseLike): void; +>g : Symbol(g, Decl(asyncReturnObjectLiteralThisType.ts, 13, 3)) +>cb : Symbol(cb, Decl(asyncReturnObjectLiteralThisType.ts, 15, 19)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) +>PromiseLike : Symbol(PromiseLike, Decl(lib.es5.d.ts, --, --)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) + +g(async () => ({ value: 1, method() { this satisfies X; } })); +>g : Symbol(g, Decl(asyncReturnObjectLiteralThisType.ts, 13, 3)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 16, 16)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 16, 26)) +>this : Symbol(__type, Decl(asyncReturnObjectLiteralThisType.ts, 0, 8)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) + diff --git a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.types b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.types new file mode 100644 index 0000000000000..4e7c2ab6e7dff --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.types @@ -0,0 +1,123 @@ +//// [tests/cases/compiler/asyncReturnObjectLiteralThisType.ts] //// + +=== asyncReturnObjectLiteralThisType.ts === +type X = { value: number; method(): void }; +>X : X +>value : number +>method : () => void + +declare function foo(cb: () => Promise): void; +>foo : (cb: () => Promise) => void +>cb : () => Promise + +foo(async () => ({ value: 5, method() { this satisfies X; } })); +>foo(async () => ({ value: 5, method() { this satisfies X; } })) : void +>foo : (cb: () => Promise) => void +>async () => ({ value: 5, method() { this satisfies X; } }) : () => Promise<{ value: number; method(): void; }> +>({ value: 5, method() { this satisfies X; } }) : { value: number; method(): void; } +>{ value: 5, method() { this satisfies X; } } : { value: number; method(): void; } +>value : number +>5 : 5 +>method : () => void +>this satisfies X : X +>this : X + +foo(async () => { return { value: 5, method() { this satisfies X; } }; }); +>foo(async () => { return { value: 5, method() { this satisfies X; } }; }) : void +>foo : (cb: () => Promise) => void +>async () => { return { value: 5, method() { this satisfies X; } }; } : () => Promise<{ value: number; method(): void; }> +>{ value: 5, method() { this satisfies X; } } : { value: number; method(): void; } +>value : number +>5 : 5 +>method : () => void +>this satisfies X : X +>this : X + +foo(async function () { return { value: 5, method() { this satisfies X; } }; }); +>foo(async function () { return { value: 5, method() { this satisfies X; } }; }) : void +>foo : (cb: () => Promise) => void +>async function () { return { value: 5, method() { this satisfies X; } }; } : () => Promise<{ value: number; method(): void; }> +>{ value: 5, method() { this satisfies X; } } : { value: number; method(): void; } +>value : number +>5 : 5 +>method : () => void +>this satisfies X : X +>this : X + +async function f(): Promise { return { value: 1, method() { this satisfies X; } }; } +>f : () => Promise +>{ value: 1, method() { this satisfies X; } } : { value: number; method(): void; } +>value : number +>1 : 1 +>method : () => void +>this satisfies X : X +>this : X + +const test2: () => Promise = async () => { return { value: 5, method() { this satisfies X; } }; }; +>test2 : () => Promise +>async () => { return { value: 5, method() { this satisfies X; } }; } : () => Promise<{ value: number; method(): void; }> +>{ value: 5, method() { this satisfies X; } } : { value: number; method(): void; } +>value : number +>5 : 5 +>method : () => void +>this satisfies X : X +>this : X + +// nullable awaited +const n1: () => Promise = async () => ({ value: 1, method() { this satisfies X; } }); +>n1 : () => Promise +>async () => ({ value: 1, method() { this satisfies X; } }) : () => Promise<{ value: number; method(): void; }> +>({ value: 1, method() { this satisfies X; } }) : { value: number; method(): void; } +>{ value: 1, method() { this satisfies X; } } : { value: number; method(): void; } +>value : number +>1 : 1 +>method : () => void +>this satisfies X : X +>this : X + +const n2: () => Promise = async () => { return { value: 1, method() { this satisfies X; } }; }; +>n2 : () => Promise +>async () => { return { value: 1, method() { this satisfies X; } }; } : () => Promise<{ value: number; method(): void; }> +>{ value: 1, method() { this satisfies X; } } : { value: number; method(): void; } +>value : number +>1 : 1 +>method : () => void +>this satisfies X : X +>this : X + +// thenable must not get this: X +const t: () => Promise<{ value: number }> = async () => ({ +>t : () => Promise<{ value: number; }> +>value : number +>async () => ({ then(resolve: any) { this.value.toFixed(); },}) : () => Promise +>({ then(resolve: any) { this.value.toFixed(); },}) : { then(resolve: any): void; } +>{ then(resolve: any) { this.value.toFixed(); },} : { then(resolve: any): void; } + + then(resolve: any) { this.value.toFixed(); }, +>then : (resolve: any) => void +>resolve : any +>this.value.toFixed() : any +>this.value.toFixed : any +>this.value : any +>this : PromiseLike<{ value: number; }> | { value: number; } +>value : any +>toFixed : any + +}); +// declared PromiseLike remains +declare function g(cb: () => X | PromiseLike): void; +>g : (cb: () => X | PromiseLike) => void +>cb : () => X | PromiseLike + +g(async () => ({ value: 1, method() { this satisfies X; } })); +>g(async () => ({ value: 1, method() { this satisfies X; } })) : void +>g : (cb: () => X | PromiseLike) => void +>async () => ({ value: 1, method() { this satisfies X; } }) : () => Promise<{ value: number; method(): void; }> +>({ value: 1, method() { this satisfies X; } }) : { value: number; method(): void; } +>{ value: 1, method() { this satisfies X; } } : { value: number; method(): void; } +>value : number +>1 : 1 +>method : () => void +>this satisfies X : X +>this : X + diff --git a/tsc/testdata/tests/cases/compiler/asyncReturnObjectLiteralThisType.ts b/tsc/testdata/tests/cases/compiler/asyncReturnObjectLiteralThisType.ts new file mode 100644 index 0000000000000..03ebf6b5d698d --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/asyncReturnObjectLiteralThisType.ts @@ -0,0 +1,21 @@ +// @strict: true +// @target: esnext +// @noEmit: true + +type X = { value: number; method(): void }; +declare function foo(cb: () => Promise): void; +foo(async () => ({ value: 5, method() { this satisfies X; } })); +foo(async () => { return { value: 5, method() { this satisfies X; } }; }); +foo(async function () { return { value: 5, method() { this satisfies X; } }; }); +async function f(): Promise { return { value: 1, method() { this satisfies X; } }; } +const test2: () => Promise = async () => { return { value: 5, method() { this satisfies X; } }; }; +// nullable awaited +const n1: () => Promise = async () => ({ value: 1, method() { this satisfies X; } }); +const n2: () => Promise = async () => { return { value: 1, method() { this satisfies X; } }; }; +// thenable must not get this: X +const t: () => Promise<{ value: number }> = async () => ({ + then(resolve: any) { this.value.toFixed(); }, +}); +// declared PromiseLike remains +declare function g(cb: () => X | PromiseLike): void; +g(async () => ({ value: 1, method() { this satisfies X; } })); From 2611862e84aafe4ef56d46c942c47c8af970870c Mon Sep 17 00:00:00 2001 From: Harsh Vardhan Chauhan Date: Sat, 31 Oct 2026 23:13:25 +0530 Subject: [PATCH 3/4] Treat spread, computed and possibly-callable 'then' as thenable; ignore non-callable 'then' --- tsc/internal/checker/checker.go | 47 +++++++++-- ...syncReturnObjectLiteralThisType.errors.txt | 28 ++++++- .../asyncReturnObjectLiteralThisType.symbols | 61 +++++++++++++++ .../asyncReturnObjectLiteralThisType.types | 78 +++++++++++++++++++ .../asyncReturnObjectLiteralThisType.ts | 16 ++++ 5 files changed, 223 insertions(+), 7 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 445d83706f2a6..7b3b8f6289530 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -12330,12 +12330,8 @@ func (c *Checker) removeSynthesizedPromiseLike(literal *ast.Node, contextualType if fn == nil { return contextualType } - for _, p := range literal.AsObjectLiteralExpression().Properties.Nodes { - if p.Name() != nil { - if name, ok := ast.TryGetTextOfPropertyName(p.Name()); ok && name == "then" { - return contextualType - } - } + if objectLiteralMayBeThenable(literal) { + return contextualType } returnType := c.getContextualReturnType(fn, ContextFlagsNone) if returnType == nil { @@ -12349,6 +12345,45 @@ func (c *Checker) removeSynthesizedPromiseLike(literal *ast.Node, contextualType return c.filterType(contextualType, func(t *Type) bool { return t != synthesized }) } +// Conservatively determines whether an object literal might be a thenable (i.e. have a callable `then` member). +// Spread members, property names that can't be resolved syntactically, and `then` members whose initializers +// aren't known to be non-callable are all treated as possibly thenable. This avoids resolving types, which could +// be circular while we're computing the contextual type. +func objectLiteralMayBeThenable(literal *ast.Node) bool { + for _, p := range literal.AsObjectLiteralExpression().Properties.Nodes { + if p.Kind == ast.KindSpreadAssignment { + return true + } + name := p.Name() + if name == nil { + continue + } + text, ok := ast.TryGetTextOfPropertyName(name) + if !ok { + return true + } + if text != "then" { + continue + } + if p.Kind == ast.KindPropertyAssignment && isKnownNonCallableExpression(p.Initializer()) { + continue + } + return true + } + return false +} + +func isKnownNonCallableExpression(node *ast.Node) bool { + node = ast.SkipParentheses(node) + switch node.Kind { + case ast.KindNumericLiteral, ast.KindBigIntLiteral, ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral, + ast.KindTemplateExpression, ast.KindTrueKeyword, ast.KindFalseKeyword, ast.KindNullKeyword, + ast.KindObjectLiteralExpression, ast.KindArrayLiteralExpression, ast.KindRegularExpressionLiteral: + return true + } + return false +} + // Returns the async, non-generator function whose return value (a `return` operand or concise arrow body) is node. func getAsyncFunctionReturningExpression(node *ast.Node) *ast.Node { parent := node.Parent diff --git a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.errors.txt b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.errors.txt index dea78912b1eaa..e4e35f66159af 100644 --- a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.errors.txt +++ b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.errors.txt @@ -1,9 +1,13 @@ asyncReturnObjectLiteralThisType.ts(12,45): error TS1058: The return type of an async function must either be a valid promise or must not contain a callable 'then' member. asyncReturnObjectLiteralThisType.ts(13,29): error TS2339: Property 'value' does not exist on type 'PromiseLike<{ value: number; }> | { value: number; }'. Property 'value' does not exist on type 'PromiseLike<{ value: number; }>'. +asyncReturnObjectLiteralThisType.ts(28,41): error TS2339: Property 'value' does not exist on type 'PromiseLike | V'. + Property 'value' does not exist on type 'PromiseLike'. +asyncReturnObjectLiteralThisType.ts(32,18): error TS2339: Property 'value' does not exist on type 'PromiseLike | V'. + Property 'value' does not exist on type 'PromiseLike'. -==== asyncReturnObjectLiteralThisType.ts (2 errors) ==== +==== asyncReturnObjectLiteralThisType.ts (4 errors) ==== type X = { value: number; method(): void }; declare function foo(cb: () => Promise): void; foo(async () => ({ value: 5, method() { this satisfies X; } })); @@ -28,4 +32,26 @@ asyncReturnObjectLiteralThisType.ts(13,29): error TS2339: Property 'value' does // declared PromiseLike remains declare function g(cb: () => X | PromiseLike): void; g(async () => ({ value: 1, method() { this satisfies X; } })); + + // non-callable `then` is not a thenable + type Y = { value: number; then: number; method(): void }; + const nonCallableThen: () => Promise = async () => ({ value: 1, then: 0, method() { this.value.toFixed(); } }); + + // thenables supplied by a computed key or a spread keep the PromiseLike branch + type V = { value: number }; + const key = "then"; + declare const thenableSource: { then(resolve: (x: V) => void): void }; + const computedThen: () => Promise = async () => ({ + [key](resolve: (x: V) => void) { this.value.toFixed(); }, + ~~~~~ +!!! error TS2339: Property 'value' does not exist on type 'PromiseLike | V'. +!!! error TS2339: Property 'value' does not exist on type 'PromiseLike'. + }); + const spreadThen: () => Promise = async () => ({ + ...thenableSource, + extra() { this.value.toFixed(); }, + ~~~~~ +!!! error TS2339: Property 'value' does not exist on type 'PromiseLike | V'. +!!! error TS2339: Property 'value' does not exist on type 'PromiseLike'. + }); \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.symbols b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.symbols index 14da480bc5700..fc24930e53c87 100644 --- a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.symbols +++ b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.symbols @@ -96,3 +96,64 @@ g(async () => ({ value: 1, method() { this satisfies X; } })); >this : Symbol(__type, Decl(asyncReturnObjectLiteralThisType.ts, 0, 8)) >X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) +// non-callable `then` is not a thenable +type Y = { value: number; then: number; method(): void }; +>Y : Symbol(Y, Decl(asyncReturnObjectLiteralThisType.ts, 16, 62)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 19, 10)) +>then : Symbol(then, Decl(asyncReturnObjectLiteralThisType.ts, 19, 25)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 19, 39)) + +const nonCallableThen: () => Promise = async () => ({ value: 1, then: 0, method() { this.value.toFixed(); } }); +>nonCallableThen : Symbol(nonCallableThen, Decl(asyncReturnObjectLiteralThisType.ts, 20, 5)) +>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2018.promise.d.ts, --, --)) +>Y : Symbol(Y, Decl(asyncReturnObjectLiteralThisType.ts, 16, 62)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 20, 56)) +>then : Symbol(then, Decl(asyncReturnObjectLiteralThisType.ts, 20, 66)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 20, 75)) +>this.value.toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --)) +>this.value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 19, 10)) +>this : Symbol(__type, Decl(asyncReturnObjectLiteralThisType.ts, 19, 8)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 19, 10)) +>toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --)) + +// thenables supplied by a computed key or a spread keep the PromiseLike branch +type V = { value: number }; +>V : Symbol(V, Decl(asyncReturnObjectLiteralThisType.ts, 20, 114)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 23, 10)) + +const key = "then"; +>key : Symbol(key, Decl(asyncReturnObjectLiteralThisType.ts, 24, 5)) + +declare const thenableSource: { then(resolve: (x: V) => void): void }; +>thenableSource : Symbol(thenableSource, Decl(asyncReturnObjectLiteralThisType.ts, 25, 13)) +>then : Symbol(then, Decl(asyncReturnObjectLiteralThisType.ts, 25, 31)) +>resolve : Symbol(resolve, Decl(asyncReturnObjectLiteralThisType.ts, 25, 37)) +>x : Symbol(x, Decl(asyncReturnObjectLiteralThisType.ts, 25, 47)) +>V : Symbol(V, Decl(asyncReturnObjectLiteralThisType.ts, 20, 114)) + +const computedThen: () => Promise = async () => ({ +>computedThen : Symbol(computedThen, Decl(asyncReturnObjectLiteralThisType.ts, 26, 5)) +>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2018.promise.d.ts, --, --)) +>V : Symbol(V, Decl(asyncReturnObjectLiteralThisType.ts, 20, 114)) + + [key](resolve: (x: V) => void) { this.value.toFixed(); }, +>[key] : Symbol([key], Decl(asyncReturnObjectLiteralThisType.ts, 26, 53)) +>key : Symbol(key, Decl(asyncReturnObjectLiteralThisType.ts, 24, 5)) +>resolve : Symbol(resolve, Decl(asyncReturnObjectLiteralThisType.ts, 27, 8)) +>x : Symbol(x, Decl(asyncReturnObjectLiteralThisType.ts, 27, 18)) +>V : Symbol(V, Decl(asyncReturnObjectLiteralThisType.ts, 20, 114)) + +}); +const spreadThen: () => Promise = async () => ({ +>spreadThen : Symbol(spreadThen, Decl(asyncReturnObjectLiteralThisType.ts, 29, 5)) +>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2018.promise.d.ts, --, --)) +>V : Symbol(V, Decl(asyncReturnObjectLiteralThisType.ts, 20, 114)) + + ...thenableSource, +>thenableSource : Symbol(thenableSource, Decl(asyncReturnObjectLiteralThisType.ts, 25, 13)) + + extra() { this.value.toFixed(); }, +>extra : Symbol(extra, Decl(asyncReturnObjectLiteralThisType.ts, 30, 20)) + +}); + diff --git a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.types b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.types index 4e7c2ab6e7dff..bfcc62c499342 100644 --- a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.types +++ b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.types @@ -121,3 +121,81 @@ g(async () => ({ value: 1, method() { this satisfies X; } })); >this satisfies X : X >this : X +// non-callable `then` is not a thenable +type Y = { value: number; then: number; method(): void }; +>Y : Y +>value : number +>then : number +>method : () => void + +const nonCallableThen: () => Promise = async () => ({ value: 1, then: 0, method() { this.value.toFixed(); } }); +>nonCallableThen : () => Promise +>async () => ({ value: 1, then: 0, method() { this.value.toFixed(); } }) : () => Promise<{ value: number; then: number; method(): void; }> +>({ value: 1, then: 0, method() { this.value.toFixed(); } }) : { value: number; then: number; method(): void; } +>{ value: 1, then: 0, method() { this.value.toFixed(); } } : { value: number; then: number; method(): void; } +>value : number +>1 : 1 +>then : number +>0 : 0 +>method : () => void +>this.value.toFixed() : string +>this.value.toFixed : (fractionDigits?: number) => string +>this.value : number +>this : Y +>value : number +>toFixed : (fractionDigits?: number) => string + +// thenables supplied by a computed key or a spread keep the PromiseLike branch +type V = { value: number }; +>V : V +>value : number + +const key = "then"; +>key : "then" +>"then" : "then" + +declare const thenableSource: { then(resolve: (x: V) => void): void }; +>thenableSource : { then(resolve: (x: V) => void): void; } +>then : (resolve: (x: V) => void) => void +>resolve : (x: V) => void +>x : V + +const computedThen: () => Promise = async () => ({ +>computedThen : () => Promise +>async () => ({ [key](resolve: (x: V) => void) { this.value.toFixed(); },}) : () => Promise +>({ [key](resolve: (x: V) => void) { this.value.toFixed(); },}) : { then(resolve: (x: V) => void): void; } +>{ [key](resolve: (x: V) => void) { this.value.toFixed(); },} : { then(resolve: (x: V) => void): void; } + + [key](resolve: (x: V) => void) { this.value.toFixed(); }, +>[key] : (resolve: (x: V) => void) => void +>key : "then" +>resolve : (x: V) => void +>x : V +>this.value.toFixed() : any +>this.value.toFixed : any +>this.value : any +>this : PromiseLike | V +>value : any +>toFixed : any + +}); +const spreadThen: () => Promise = async () => ({ +>spreadThen : () => Promise +>async () => ({ ...thenableSource, extra() { this.value.toFixed(); },}) : () => Promise +>({ ...thenableSource, extra() { this.value.toFixed(); },}) : { then(resolve: (x: V) => void): void; extra(): void; } +>{ ...thenableSource, extra() { this.value.toFixed(); },} : { then(resolve: (x: V) => void): void; extra(): void; } + + ...thenableSource, +>thenableSource : { then(resolve: (x: V) => void): void; } + + extra() { this.value.toFixed(); }, +>extra : () => void +>this.value.toFixed() : any +>this.value.toFixed : any +>this.value : any +>this : PromiseLike | V +>value : any +>toFixed : any + +}); + diff --git a/tsc/testdata/tests/cases/compiler/asyncReturnObjectLiteralThisType.ts b/tsc/testdata/tests/cases/compiler/asyncReturnObjectLiteralThisType.ts index 03ebf6b5d698d..c8a541d631cda 100644 --- a/tsc/testdata/tests/cases/compiler/asyncReturnObjectLiteralThisType.ts +++ b/tsc/testdata/tests/cases/compiler/asyncReturnObjectLiteralThisType.ts @@ -19,3 +19,19 @@ const t: () => Promise<{ value: number }> = async () => ({ // declared PromiseLike remains declare function g(cb: () => X | PromiseLike): void; g(async () => ({ value: 1, method() { this satisfies X; } })); + +// non-callable `then` is not a thenable +type Y = { value: number; then: number; method(): void }; +const nonCallableThen: () => Promise = async () => ({ value: 1, then: 0, method() { this.value.toFixed(); } }); + +// thenables supplied by a computed key or a spread keep the PromiseLike branch +type V = { value: number }; +const key = "then"; +declare const thenableSource: { then(resolve: (x: V) => void): void }; +const computedThen: () => Promise = async () => ({ + [key](resolve: (x: V) => void) { this.value.toFixed(); }, +}); +const spreadThen: () => Promise = async () => ({ + ...thenableSource, + extra() { this.value.toFixed(); }, +}); From a67334b213e1763c62643e8829057378ef862b0e Mon Sep 17 00:00:00 2001 From: Harsh Vardhan Chauhan Date: Sat, 31 Oct 2026 23:25:00 +0530 Subject: [PATCH 4/4] Inspect spread object literals and resolve computed keys when detecting thenables --- tsc/internal/checker/checker.go | 23 +++++-- ...syncReturnObjectLiteralThisType.errors.txt | 18 ++++- .../asyncReturnObjectLiteralThisType.symbols | 43 ++++++++++++ .../asyncReturnObjectLiteralThisType.types | 69 +++++++++++++++++++ .../asyncReturnObjectLiteralThisType.ts | 11 +++ 5 files changed, 157 insertions(+), 7 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 7b3b8f6289530..09be73451771e 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -12330,7 +12330,7 @@ func (c *Checker) removeSynthesizedPromiseLike(literal *ast.Node, contextualType if fn == nil { return contextualType } - if objectLiteralMayBeThenable(literal) { + if c.objectLiteralMayBeThenable(literal) { return contextualType } returnType := c.getContextualReturnType(fn, ContextFlagsNone) @@ -12346,12 +12346,16 @@ func (c *Checker) removeSynthesizedPromiseLike(literal *ast.Node, contextualType } // Conservatively determines whether an object literal might be a thenable (i.e. have a callable `then` member). -// Spread members, property names that can't be resolved syntactically, and `then` members whose initializers -// aren't known to be non-callable are all treated as possibly thenable. This avoids resolving types, which could -// be circular while we're computing the contextual type. -func objectLiteralMayBeThenable(literal *ast.Node) bool { +// Spreads of non-literal expressions, property names that can't be statically resolved, and `then` members whose +// initializers aren't known to be non-callable are all treated as possibly thenable. Spreads of object literals are +// inspected recursively, and computed names are resolved through their (cached) computed-name type. +func (c *Checker) objectLiteralMayBeThenable(literal *ast.Node) bool { for _, p := range literal.AsObjectLiteralExpression().Properties.Nodes { if p.Kind == ast.KindSpreadAssignment { + expression := ast.SkipParentheses(p.Expression()) + if expression.Kind == ast.KindObjectLiteralExpression && !c.objectLiteralMayBeThenable(expression) { + continue + } return true } name := p.Name() @@ -12360,7 +12364,14 @@ func objectLiteralMayBeThenable(literal *ast.Node) bool { } text, ok := ast.TryGetTextOfPropertyName(name) if !ok { - return true + if name.Kind != ast.KindComputedPropertyName { + return true + } + nameType := c.checkComputedPropertyName(name) + if !isTypeUsableAsPropertyName(nameType) { + return true + } + text = getPropertyNameFromType(nameType) } if text != "then" { continue diff --git a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.errors.txt b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.errors.txt index e4e35f66159af..f5c5b6aaf782a 100644 --- a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.errors.txt +++ b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.errors.txt @@ -5,9 +5,11 @@ asyncReturnObjectLiteralThisType.ts(28,41): error TS2339: Property 'value' does Property 'value' does not exist on type 'PromiseLike'. asyncReturnObjectLiteralThisType.ts(32,18): error TS2339: Property 'value' does not exist on type 'PromiseLike | V'. Property 'value' does not exist on type 'PromiseLike'. +asyncReturnObjectLiteralThisType.ts(43,18): error TS2339: Property 'value' does not exist on type 'PromiseLike | V'. + Property 'value' does not exist on type 'PromiseLike'. -==== asyncReturnObjectLiteralThisType.ts (4 errors) ==== +==== asyncReturnObjectLiteralThisType.ts (5 errors) ==== type X = { value: number; method(): void }; declare function foo(cb: () => Promise): void; foo(async () => ({ value: 5, method() { this satisfies X; } })); @@ -52,6 +54,20 @@ asyncReturnObjectLiteralThisType.ts(32,18): error TS2339: Property 'value' does extra() { this.value.toFixed(); }, ~~~~~ !!! error TS2339: Property 'value' does not exist on type 'PromiseLike | V'. +!!! error TS2339: Property 'value' does not exist on type 'PromiseLike'. + }); + + // spreads of object literals and statically known computed keys other than `then` are not thenables + const valueKey = "value"; + foo(async () => ({ ...{ value: 5 }, method() { this satisfies X; } })); + foo(async () => ({ ...({ value: 5 }), method() { this satisfies X; } })); + foo(async () => ({ [valueKey]: 5, method() { this satisfies X; } })); + // a spread object literal containing `then` is still a thenable + const nestedSpreadThen: () => Promise = async () => ({ + ...{ then(resolve: (x: V) => void) {} }, + extra() { this.value.toFixed(); }, + ~~~~~ +!!! error TS2339: Property 'value' does not exist on type 'PromiseLike | V'. !!! error TS2339: Property 'value' does not exist on type 'PromiseLike'. }); \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.symbols b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.symbols index fc24930e53c87..cb050df5d4e65 100644 --- a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.symbols +++ b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.symbols @@ -157,3 +157,46 @@ const spreadThen: () => Promise = async () => ({ }); +// spreads of object literals and statically known computed keys other than `then` are not thenables +const valueKey = "value"; +>valueKey : Symbol(valueKey, Decl(asyncReturnObjectLiteralThisType.ts, 35, 5)) + +foo(async () => ({ ...{ value: 5 }, method() { this satisfies X; } })); +>foo : Symbol(foo, Decl(asyncReturnObjectLiteralThisType.ts, 0, 43)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 36, 23)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 36, 35)) +>this : Symbol(__type, Decl(asyncReturnObjectLiteralThisType.ts, 0, 8)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) + +foo(async () => ({ ...({ value: 5 }), method() { this satisfies X; } })); +>foo : Symbol(foo, Decl(asyncReturnObjectLiteralThisType.ts, 0, 43)) +>value : Symbol(value, Decl(asyncReturnObjectLiteralThisType.ts, 37, 24)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 37, 37)) +>this : Symbol(__type, Decl(asyncReturnObjectLiteralThisType.ts, 0, 8)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) + +foo(async () => ({ [valueKey]: 5, method() { this satisfies X; } })); +>foo : Symbol(foo, Decl(asyncReturnObjectLiteralThisType.ts, 0, 43)) +>[valueKey] : Symbol([valueKey], Decl(asyncReturnObjectLiteralThisType.ts, 38, 18)) +>valueKey : Symbol(valueKey, Decl(asyncReturnObjectLiteralThisType.ts, 35, 5)) +>method : Symbol(method, Decl(asyncReturnObjectLiteralThisType.ts, 38, 33)) +>this : Symbol(__type, Decl(asyncReturnObjectLiteralThisType.ts, 0, 8)) +>X : Symbol(X, Decl(asyncReturnObjectLiteralThisType.ts, 0, 0)) + +// a spread object literal containing `then` is still a thenable +const nestedSpreadThen: () => Promise = async () => ({ +>nestedSpreadThen : Symbol(nestedSpreadThen, Decl(asyncReturnObjectLiteralThisType.ts, 40, 5)) +>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2018.promise.d.ts, --, --)) +>V : Symbol(V, Decl(asyncReturnObjectLiteralThisType.ts, 20, 114)) + + ...{ then(resolve: (x: V) => void) {} }, +>then : Symbol(then, Decl(asyncReturnObjectLiteralThisType.ts, 41, 6)) +>resolve : Symbol(resolve, Decl(asyncReturnObjectLiteralThisType.ts, 41, 12)) +>x : Symbol(x, Decl(asyncReturnObjectLiteralThisType.ts, 41, 22)) +>V : Symbol(V, Decl(asyncReturnObjectLiteralThisType.ts, 20, 114)) + + extra() { this.value.toFixed(); }, +>extra : Symbol(extra, Decl(asyncReturnObjectLiteralThisType.ts, 41, 42)) + +}); + diff --git a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.types b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.types index bfcc62c499342..c49a06e803e47 100644 --- a/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.types +++ b/tsc/testdata/baselines/reference/compiler/asyncReturnObjectLiteralThisType.types @@ -199,3 +199,72 @@ const spreadThen: () => Promise = async () => ({ }); +// spreads of object literals and statically known computed keys other than `then` are not thenables +const valueKey = "value"; +>valueKey : "value" +>"value" : "value" + +foo(async () => ({ ...{ value: 5 }, method() { this satisfies X; } })); +>foo(async () => ({ ...{ value: 5 }, method() { this satisfies X; } })) : void +>foo : (cb: () => Promise) => void +>async () => ({ ...{ value: 5 }, method() { this satisfies X; } }) : () => Promise<{ value: number; method(): void; }> +>({ ...{ value: 5 }, method() { this satisfies X; } }) : { value: number; method(): void; } +>{ ...{ value: 5 }, method() { this satisfies X; } } : { value: number; method(): void; } +>{ value: 5 } : { value: number; } +>value : number +>5 : 5 +>method : () => void +>this satisfies X : X +>this : X + +foo(async () => ({ ...({ value: 5 }), method() { this satisfies X; } })); +>foo(async () => ({ ...({ value: 5 }), method() { this satisfies X; } })) : void +>foo : (cb: () => Promise) => void +>async () => ({ ...({ value: 5 }), method() { this satisfies X; } }) : () => Promise<{ value: number; method(): void; }> +>({ ...({ value: 5 }), method() { this satisfies X; } }) : { value: number; method(): void; } +>{ ...({ value: 5 }), method() { this satisfies X; } } : { value: number; method(): void; } +>({ value: 5 }) : { value: number; } +>{ value: 5 } : { value: number; } +>value : number +>5 : 5 +>method : () => void +>this satisfies X : X +>this : X + +foo(async () => ({ [valueKey]: 5, method() { this satisfies X; } })); +>foo(async () => ({ [valueKey]: 5, method() { this satisfies X; } })) : void +>foo : (cb: () => Promise) => void +>async () => ({ [valueKey]: 5, method() { this satisfies X; } }) : () => Promise<{ value: number; method(): void; }> +>({ [valueKey]: 5, method() { this satisfies X; } }) : { value: number; method(): void; } +>{ [valueKey]: 5, method() { this satisfies X; } } : { value: number; method(): void; } +>[valueKey] : number +>valueKey : "value" +>5 : 5 +>method : () => void +>this satisfies X : X +>this : X + +// a spread object literal containing `then` is still a thenable +const nestedSpreadThen: () => Promise = async () => ({ +>nestedSpreadThen : () => Promise +>async () => ({ ...{ then(resolve: (x: V) => void) {} }, extra() { this.value.toFixed(); },}) : () => Promise +>({ ...{ then(resolve: (x: V) => void) {} }, extra() { this.value.toFixed(); },}) : { then(resolve: (x: V) => void): void; extra(): void; } +>{ ...{ then(resolve: (x: V) => void) {} }, extra() { this.value.toFixed(); },} : { then(resolve: (x: V) => void): void; extra(): void; } + + ...{ then(resolve: (x: V) => void) {} }, +>{ then(resolve: (x: V) => void) {} } : { then(resolve: (x: V) => void): void; } +>then : (resolve: (x: V) => void) => void +>resolve : (x: V) => void +>x : V + + extra() { this.value.toFixed(); }, +>extra : () => void +>this.value.toFixed() : any +>this.value.toFixed : any +>this.value : any +>this : PromiseLike | V +>value : any +>toFixed : any + +}); + diff --git a/tsc/testdata/tests/cases/compiler/asyncReturnObjectLiteralThisType.ts b/tsc/testdata/tests/cases/compiler/asyncReturnObjectLiteralThisType.ts index c8a541d631cda..250ed7fd92286 100644 --- a/tsc/testdata/tests/cases/compiler/asyncReturnObjectLiteralThisType.ts +++ b/tsc/testdata/tests/cases/compiler/asyncReturnObjectLiteralThisType.ts @@ -35,3 +35,14 @@ const spreadThen: () => Promise = async () => ({ ...thenableSource, extra() { this.value.toFixed(); }, }); + +// spreads of object literals and statically known computed keys other than `then` are not thenables +const valueKey = "value"; +foo(async () => ({ ...{ value: 5 }, method() { this satisfies X; } })); +foo(async () => ({ ...({ value: 5 }), method() { this satisfies X; } })); +foo(async () => ({ [valueKey]: 5, method() { this satisfies X; } })); +// a spread object literal containing `then` is still a thenable +const nestedSpreadThen: () => Promise = async () => ({ + ...{ then(resolve: (x: V) => void) {} }, + extra() { this.value.toFixed(); }, +});