Give an async object-literal method its contextual return type

An async method in an object literal with no return annotation and a
contextual signature inferred its own (narrower) literal type, while its
`return` literal was built at the contextually typed record: the return
statement then had to convert the wider record into the narrower literal,
which fails with SC2002. The method now takes its return type from the
contextual property's call signature, as the arrow form does. Sync
methods are unchanged; they already adopt their slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-28 19:55:50 -07:00
co-authored by Claude Opus 5.5
parent 9eaa210e1f
commit 2acd199fbe
4 changed files with 48 additions and 1 deletions
@@ -164,7 +164,33 @@ export function declaredReturnType(
if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && ts.isGetAccessorDeclaration(decl)) return DYN;
const sig = lowerer.checker.getSignatureFromDeclaration(decl);
if (!sig) lowerer.unsupported("SC1090", decl, "this function form");
const retTsType = lowerer.checker.getReturnTypeOfSignature(sig);
let retTsType = lowerer.checker.getReturnTypeOfSignature(sig);
// An ASYNC object-literal method with no return annotation and a
// contextual signature takes its return from that signature, as an async
// arrow does: the body's `return` builds at the contextually typed
// literal, so the checker's own inferred literal (narrower than the
// contextual record) could never hold it.
if (
ts.isMethodDeclaration(decl) &&
ts.isObjectLiteralExpression(decl.parent) &&
decl.type === undefined &&
decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) === true
) {
const contextual = lowerer.checker.getContextualType(decl.parent);
const slot =
contextual === undefined || (!ts.isIdentifier(decl.name) && !ts.isStringLiteral(decl.name))
? undefined
: lowerer.checker.getPropertyOfType(
lowerer.checker.getNonNullableType(contextual),
decl.name.text,
);
const signatures =
slot === undefined
? []
: lowerer.checker.getCallSignatures(lowerer.checker.getTypeOfSymbol(slot));
if (signatures.length === 1)
retTsType = lowerer.checker.getReturnTypeOfSignature(signatures[0]!);
}
// An inferred null/undefined return in JS can read a mutable checked-value
// field. Keep the actual value instead of restricting it to the initializer.
if (
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/async-object-method-contextual-return.ts"
],
"diags": []
}
@@ -0,0 +1,14 @@
type Text = { type: "text"; text: string; textSignature?: string };
type Image = { type: "image"; data: string; mimeType: string };
interface ToolResult<D> { content: (Text | Image)[]; details: D; isError?: boolean }
interface ToolDefinition<D> { name: string; execute(id: string, q: string): Promise<ToolResult<D>> }
function define(): ToolDefinition<{ loaded: string[] }> {
return {
name: "search",
async execute(_id, q) {
const text = `found ${q}`;
return { content: [{ type: "text", text }], details: { loaded: [q] } };
},
};
}
define().execute("1", "x").then((r) => console.log(JSON.stringify(r)));
@@ -0,0 +1 @@
{ "plain": 3268, "san": 6404 }