diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index ad787fb62495d..33461c9f26188 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1822,6 +1822,15 @@ export class Checker { return data ? this.objectRegistry.getOrCreateType(data) : undefined; } + async getAwaitedType(type: Type): Promise { + const data = await this.client.apiRequest("getAwaitedType", { + snapshot: this.snapshotId, + project: this.project.id, + type: type.id, + }); + return data ? this.objectRegistry.getOrCreateType(data) : undefined; + } + async getPropertyOfType(type: Type, name: string): Promise { const data = await this.client.apiRequest("getPropertyOfType", { snapshot: this.snapshotId, diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 35c08679d2aa4..2cfc1b940d00c 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -91,6 +91,7 @@ export interface APIMethodInfo { getPropertiesOfType: APIMethod; getApparentPropertiesOfType: APIMethod; getApparentType: APIMethod; + getAwaitedType: APIMethod; getReducedType: APIMethod; getPropertyOfType: APIMethod; getIndexInfosOfType: APIMethod; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 074471e86cfbb..f63eccd8d07ee 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -1830,6 +1830,15 @@ export class Checker { return data ? this.objectRegistry.getOrCreateType(data) : undefined; } + getAwaitedType(type: Type): Type | undefined { + const data = this.client.apiRequest("getAwaitedType", { + snapshot: this.snapshotId, + project: this.project.id, + type: type.id, + }); + return data ? this.objectRegistry.getOrCreateType(data) : undefined; + } + getPropertyOfType(type: Type, name: string): Symbol | undefined { const data = this.client.apiRequest("getPropertyOfType", { snapshot: this.snapshotId, diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index fe08206bc486b..cf15a3c9c9f1f 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -4112,6 +4112,91 @@ describe("Checker - getBaseConstraintOfType", () => { }); }); +describe("Checker - getAwaitedType", () => { + test("unwraps Promise to string", async () => { + const src = `export const value: Promise = Promise.resolve("x");`; + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": src, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("value")); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + const awaited = await project.checker.getAwaitedType(type); + assert.ok(awaited); + assert.ok(awaited.flags & TypeFlags.String, `Expected string, got flags ${awaited.flags}`); + } + finally { + await api.close(); + } + }); + + test("unwraps nested Promise>", async () => { + const src = `export const value: Promise> = Promise.resolve(Promise.resolve(1));`; + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": src, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("value")); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + const awaited = await project.checker.getAwaitedType(type); + assert.ok(awaited); + assert.ok(awaited.flags & TypeFlags.Number, `Expected number, got flags ${awaited.flags}`); + } + finally { + await api.close(); + } + }); + + test("returns the type itself when it is not thenable", async () => { + const src = `export const value: string = "x";`; + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": src, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("value")); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + const awaited = await project.checker.getAwaitedType(type); + assert.ok(awaited); + assert.strictEqual(awaited, type); + } + finally { + await api.close(); + } + }); + + test("returns undefined for a recursive thenable", async () => { + const src = `export type Loop = { then(resolve: (value: Loop) => void): void };`; + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": src, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("Loop")); + assert.ok(symbol); + const type = await project.checker.getDeclaredTypeOfSymbol(symbol); + const awaited = await project.checker.getAwaitedType(type); + assert.equal(awaited, undefined); + } + finally { + await api.close(); + } + }); +}); + describe("Checker - getPropertyOfType", () => { test("returns a named property symbol of a type", async () => { const api = spawnAPI({ diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 6974061a4a6fb..710f8dc59f000 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -4120,6 +4120,91 @@ describe("Checker - getBaseConstraintOfType", () => { }); }); +describe("Checker - getAwaitedType", () => { + test("unwraps Promise to string", () => { + const src = `export const value: Promise = Promise.resolve("x");`; + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": src, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("value")); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + const awaited = project.checker.getAwaitedType(type); + assert.ok(awaited); + assert.ok(awaited.flags & TypeFlags.String, `Expected string, got flags ${awaited.flags}`); + } + finally { + api.close(); + } + }); + + test("unwraps nested Promise>", () => { + const src = `export const value: Promise> = Promise.resolve(Promise.resolve(1));`; + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": src, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("value")); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + const awaited = project.checker.getAwaitedType(type); + assert.ok(awaited); + assert.ok(awaited.flags & TypeFlags.Number, `Expected number, got flags ${awaited.flags}`); + } + finally { + api.close(); + } + }); + + test("returns the type itself when it is not thenable", () => { + const src = `export const value: string = "x";`; + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": src, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("value")); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + const awaited = project.checker.getAwaitedType(type); + assert.ok(awaited); + assert.strictEqual(awaited, type); + } + finally { + api.close(); + } + }); + + test("returns undefined for a recursive thenable", () => { + const src = `export type Loop = { then(resolve: (value: Loop) => void): void };`; + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": src, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const symbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("Loop")); + assert.ok(symbol); + const type = project.checker.getDeclaredTypeOfSymbol(symbol); + const awaited = project.checker.getAwaitedType(type); + assert.equal(awaited, undefined); + } + finally { + api.close(); + } + }); +}); + describe("Checker - getPropertyOfType", () => { test("returns a named property symbol of a type", () => { const api = spawnAPI({ diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 45793d96a61be..6665fa2f1984f 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -149,6 +149,7 @@ const ( MethodGetPropertiesOfType Method = "getPropertiesOfType" MethodGetApparentPropertiesOfType Method = "getApparentPropertiesOfType" MethodGetApparentType Method = "getApparentType" + MethodGetAwaitedType Method = "getAwaitedType" MethodGetReducedType Method = "getReducedType" MethodGetPropertyOfType Method = "getPropertyOfType" MethodGetIndexInfosOfType Method = "getIndexInfosOfType" @@ -488,6 +489,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetPropertiesOfType: unmarshallerFor[CheckerTypeParams], MethodGetApparentPropertiesOfType: unmarshallerFor[GetTypePropertyParams], MethodGetApparentType: unmarshallerFor[GetTypePropertyParams], + MethodGetAwaitedType: unmarshallerFor[CheckerTypeParams], MethodGetReducedType: unmarshallerFor[GetTypePropertyParams], MethodGetPropertyOfType: unmarshallerFor[GetPropertyOfTypeParams], MethodGetIndexInfosOfType: unmarshallerFor[CheckerTypeParams], diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index d5c668385b849..8f45fcedec4d8 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -777,6 +777,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetApparentPropertiesOfType(ctx, parsed.(*GetTypePropertyParams)) case string(MethodGetApparentType): return s.handleGetApparentType(ctx, parsed.(*GetTypePropertyParams)) + case string(MethodGetAwaitedType): + return s.handleGetAwaitedType(ctx, parsed.(*CheckerTypeParams)) case string(MethodGetReducedType): return s.handleGetReducedType(ctx, parsed.(*GetTypePropertyParams)) case string(MethodGetPropertyOfType): @@ -3064,6 +3066,28 @@ func (s *Session) handleGetApparentType(ctx context.Context, params *GetTypeProp return setup.newTypeResponse(setup.checker.GetApparentType(t)), nil } +// handleGetAwaitedType returns the type of `await expr` for a type. +// @gen-proto-nullable +func (s *Session) handleGetAwaitedType(ctx context.Context, params *CheckerTypeParams) (*TypeResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + t, err := setup.resolveTypeHandle(params.Type) + if err != nil { + return nil, err + } + + awaited := setup.checker.GetAwaitedType(t) + if awaited == nil { + return nil, nil + } + + return setup.newTypeResponse(awaited), nil +} + // handleGetReducedType returns the reduced type of a type. func (s *Session) handleGetReducedType(ctx context.Context, params *GetTypePropertyParams) (*TypeResponse, error) { setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) diff --git a/tsc/internal/checker/exports.go b/tsc/internal/checker/exports.go index 49efc3208d430..bfb5f7f0008c6 100644 --- a/tsc/internal/checker/exports.go +++ b/tsc/internal/checker/exports.go @@ -294,6 +294,10 @@ func (c *Checker) GetApparentType(t *Type) *Type { return c.getApparentType(t) } +func (c *Checker) GetAwaitedType(t *Type) *Type { + return c.getAwaitedType(t) +} + func (c *Checker) GetReducedType(t *Type) *Type { return c.getReducedType(t) }