diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index ad787fb62495d..5918fef0d487b 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1832,6 +1832,16 @@ export class Checker { return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; } + async getTypeOfPropertyOfType(type: Type, name: string): Promise { + const data = await this.client.apiRequest("getTypeOfPropertyOfType", { + snapshot: this.snapshotId, + project: this.project.id, + type: type.id, + name, + }); + return data ? this.objectRegistry.getOrCreateType(data) : undefined; + } + async getConstantValue(node: Node): Promise { const data = await this.client.apiRequest("getConstantValue", { snapshot: this.snapshotId, diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 35c08679d2aa4..bf5f5fde9279c 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -93,6 +93,7 @@ export interface APIMethodInfo { getApparentType: APIMethod; getReducedType: APIMethod; getPropertyOfType: APIMethod; + getTypeOfPropertyOfType: APIMethod; getIndexInfosOfType: APIMethod; getConstraintOfTypeParameter: APIMethod; getDefaultFromTypeParameter: APIMethod; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 074471e86cfbb..c9e8ed48f813b 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -1840,6 +1840,16 @@ export class Checker { return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; } + getTypeOfPropertyOfType(type: Type, name: string): Type | undefined { + const data = this.client.apiRequest("getTypeOfPropertyOfType", { + snapshot: this.snapshotId, + project: this.project.id, + type: type.id, + name, + }); + return data ? this.objectRegistry.getOrCreateType(data) : undefined; + } + getConstantValue(node: Node): string | number | undefined { const data = this.client.apiRequest("getConstantValue", { snapshot: this.snapshotId, diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index fe08206bc486b..328cd5789dc6d 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -4145,6 +4145,63 @@ export declare const p: Person; }); }); +describe("Checker - getTypeOfPropertyOfType", () => { + test("returns the type of a named property", async () => { + const src = ` +export interface Person { + name: string; + age: number; +} +export declare const p: Person; +`; + 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("p: Person")); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + const nameType = await project.checker.getTypeOfPropertyOfType(type, "name"); + assert.ok(nameType); + assert.ok(nameType.flags & TypeFlags.String, `Expected string, got flags ${nameType.flags}`); + const ageType = await project.checker.getTypeOfPropertyOfType(type, "age"); + assert.ok(ageType); + assert.ok(ageType.flags & TypeFlags.Number, `Expected number, got flags ${ageType.flags}`); + } + finally { + await api.close(); + } + }); + + test("returns undefined for a missing property", async () => { + const src = ` +export interface Person { + name: string; +} +export declare const p: Person; +`; + 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("p: Person")); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + const missing = await project.checker.getTypeOfPropertyOfType(type, "doesNotExist"); + assert.equal(missing, undefined); + } + finally { + await api.close(); + } + }); +}); + describe("Checker - getConstantValue", () => { test("returns numeric value of an enum member", async () => { const api = spawnAPI({ diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 6974061a4a6fb..d6e2fba4b8a03 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -4153,6 +4153,63 @@ export declare const p: Person; }); }); +describe("Checker - getTypeOfPropertyOfType", () => { + test("returns the type of a named property", () => { + const src = ` +export interface Person { + name: string; + age: number; +} +export declare const p: Person; +`; + 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("p: Person")); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + const nameType = project.checker.getTypeOfPropertyOfType(type, "name"); + assert.ok(nameType); + assert.ok(nameType.flags & TypeFlags.String, `Expected string, got flags ${nameType.flags}`); + const ageType = project.checker.getTypeOfPropertyOfType(type, "age"); + assert.ok(ageType); + assert.ok(ageType.flags & TypeFlags.Number, `Expected number, got flags ${ageType.flags}`); + } + finally { + api.close(); + } + }); + + test("returns undefined for a missing property", () => { + const src = ` +export interface Person { + name: string; +} +export declare const p: Person; +`; + 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("p: Person")); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + const missing = project.checker.getTypeOfPropertyOfType(type, "doesNotExist"); + assert.equal(missing, undefined); + } + finally { + api.close(); + } + }); +}); + describe("Checker - getConstantValue", () => { test("returns numeric value of an enum member", () => { const api = spawnAPI({ diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 45793d96a61be..d4e07cdecd9ae 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -151,6 +151,7 @@ const ( MethodGetApparentType Method = "getApparentType" MethodGetReducedType Method = "getReducedType" MethodGetPropertyOfType Method = "getPropertyOfType" + MethodGetTypeOfPropertyOfType Method = "getTypeOfPropertyOfType" MethodGetIndexInfosOfType Method = "getIndexInfosOfType" MethodGetConstraintOfTypeParameter Method = "getConstraintOfTypeParameter" MethodGetDefaultFromTypeParameter Method = "getDefaultFromTypeParameter" @@ -490,6 +491,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetApparentType: unmarshallerFor[GetTypePropertyParams], MethodGetReducedType: unmarshallerFor[GetTypePropertyParams], MethodGetPropertyOfType: unmarshallerFor[GetPropertyOfTypeParams], + MethodGetTypeOfPropertyOfType: unmarshallerFor[GetPropertyOfTypeParams], MethodGetIndexInfosOfType: unmarshallerFor[CheckerTypeParams], MethodGetConstraintOfTypeParameter: unmarshallerFor[GetTypePropertyParams], MethodGetBaseConstraintOfType: unmarshallerFor[CheckerTypeParams], diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index d5c668385b849..5930af5f6e131 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -781,6 +781,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetReducedType(ctx, parsed.(*GetTypePropertyParams)) case string(MethodGetPropertyOfType): return s.handleGetPropertyOfType(ctx, parsed.(*GetPropertyOfTypeParams)) + case string(MethodGetTypeOfPropertyOfType): + return s.handleGetTypeOfPropertyOfType(ctx, parsed.(*GetPropertyOfTypeParams)) case string(MethodGetIndexInfosOfType): return s.handleGetIndexInfosOfType(ctx, parsed.(*CheckerTypeParams)) case string(MethodGetConstraintOfTypeParameter): @@ -3197,6 +3199,28 @@ func (s *Session) handleGetPropertyOfType(ctx context.Context, params *GetProper return setup.newSymbolResponse(prop), nil } +// handleGetTypeOfPropertyOfType returns the type of a named property, or nil if missing. +// @gen-proto-nullable +func (s *Session) handleGetTypeOfPropertyOfType(ctx context.Context, params *GetPropertyOfTypeParams) (*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 + } + + propType := setup.checker.GetTypeOfPropertyOfType(t, params.Name) + if propType == nil { + return nil, nil + } + + return setup.newTypeResponse(propType), nil +} + // handleGetConstantValue returns the constant value of an enum member or const enum access. // @gen-proto-nullable func (s *Session) handleGetConstantValue(ctx context.Context, params *CheckerNodeParams) (any, error) {