Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -12291,6 +12291,9 @@ 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 {
contextualType = c.removeSynthesizedPromiseLike(containingLiteral, contextualType)
}
thisType = c.GetNonNullableType(contextualType)
} else {
thisType = c.checkExpressionCached(containingLiteral)
Expand Down Expand Up @@ -12318,6 +12321,107 @@ func (c *Checker) getContextualThisParameterType(fn *ast.Node) *Type {
return nil
}

// The contextual type of an async function's return expression is `T | PromiseLike<T>`, where `T` is the awaited
// contextual return type. For `this` in methods of a returned object literal, the `PromiseLike<T>` 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
}
if c.objectLiteralMayBeThenable(literal) {
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 })
}

// Conservatively determines whether an object literal might be a thenable (i.e. have a callable `then` member).
// 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()
if name == nil {
continue
}
text, ok := ast.TryGetTextOfPropertyName(name)
if !ok {
if name.Kind != ast.KindComputedPropertyName {
return true
}
nameType := c.checkComputedPropertyName(name)
if !isTypeUsableAsPropertyName(nameType) {
return true
}
text = getPropertyNameFromType(nameType)
}
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
for parent != nil && ast.IsParenthesizedExpression(parent) {
node = parent
parent = parent.Parent
}
if parent == nil {
return nil
}
var fn *ast.Node
switch {
case ast.IsReturnStatement(parent):
fn = ast.GetContainingFunction(parent)
case ast.IsArrowFunction(parent) && parent.Body() == node:
fn = parent
}
if fn == nil {
return nil
}
flags := ast.GetFunctionFlags(fn)
if flags&ast.FunctionFlagsAsync != 0 && flags&ast.FunctionFlagsGenerator == 0 {
return fn
}
return nil
}

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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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> | V'.
Property 'value' does not exist on type 'PromiseLike<V>'.
asyncReturnObjectLiteralThisType.ts(32,18): error TS2339: Property 'value' does not exist on type 'PromiseLike<V> | V'.
Property 'value' does not exist on type 'PromiseLike<V>'.
asyncReturnObjectLiteralThisType.ts(43,18): error TS2339: Property 'value' does not exist on type 'PromiseLike<V> | V'.
Property 'value' does not exist on type 'PromiseLike<V>'.


==== asyncReturnObjectLiteralThisType.ts (5 errors) ====
type X = { value: number; method(): void };
declare function foo(cb: () => Promise<X>): 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<X> { return { value: 1, method() { this satisfies X; } }; }
const test2: () => Promise<X> = async () => { return { value: 5, method() { this satisfies X; } }; };
// nullable awaited
const n1: () => Promise<X | null> = async () => ({ value: 1, method() { this satisfies X; } });
const n2: () => Promise<X | undefined> = 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<X>): 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<Y> = 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<V> = async () => ({
[key](resolve: (x: V) => void) { this.value.toFixed(); },
~~~~~
!!! error TS2339: Property 'value' does not exist on type 'PromiseLike<V> | V'.
!!! error TS2339: Property 'value' does not exist on type 'PromiseLike<V>'.
});
const spreadThen: () => Promise<V> = async () => ({
...thenableSource,
extra() { this.value.toFixed(); },
~~~~~
!!! error TS2339: Property 'value' does not exist on type 'PromiseLike<V> | V'.
!!! error TS2339: Property 'value' does not exist on type 'PromiseLike<V>'.
});

// 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<V> = async () => ({
...{ then(resolve: (x: V) => void) {} },
extra() { this.value.toFixed(); },
~~~~~
!!! error TS2339: Property 'value' does not exist on type 'PromiseLike<V> | V'.
!!! error TS2339: Property 'value' does not exist on type 'PromiseLike<V>'.
});

Loading