From 9375bf956b7b59eba656e030ec1584104e9e28aa Mon Sep 17 00:00:00 2001 From: Baptiste Jamin Date: Tue, 25 Aug 2026 14:51:17 +0200 Subject: [PATCH] Expose Checker.getIndexInfoOfType on the unstable API --- packages/typescript/src/api/async/api.ts | 16 ++++ .../typescript/src/api/proto.generated.ts | 9 ++ packages/typescript/src/api/sync/api.ts | 16 ++++ packages/typescript/test/async/api.test.ts | 84 +++++++++++++++++++ packages/typescript/test/sync/api.test.ts | 84 +++++++++++++++++++ tsc/internal/api/proto.go | 10 +++ tsc/internal/api/session.go | 37 ++++++++ 7 files changed, 256 insertions(+) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index ad787fb62495d..217a2e54b5e1e 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1801,6 +1801,22 @@ export class Checker { return type.getIndexInfos(); } + async getIndexInfoOfType(type: Type, keyType: Type): Promise { + const data = await this.client.apiRequest("getIndexInfoOfType", { + snapshot: this.snapshotId, + project: this.project.id, + type: type.id, + keyType: keyType.id, + }); + if (!data) return undefined; + return { + keyType: this.objectRegistry.getOrCreateType(data.keyType), + valueType: this.objectRegistry.getOrCreateType(data.valueType), + isReadonly: data.isReadonly ?? false, + declaration: data.declaration ? new NodeHandle(data.declaration, this.project) : undefined, + }; + } + /** * Get the constraint of a type parameter (the `T` in ``), or * undefined if it has none. diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 35c08679d2aa4..58496185d2eef 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; + getIndexInfoOfType: APIMethod; getIndexInfosOfType: APIMethod; getConstraintOfTypeParameter: APIMethod; getDefaultFromTypeParameter: APIMethod; @@ -644,6 +645,14 @@ export interface GetPropertyOfTypeParams { name: string; } +/** GetIndexInfoOfTypeParams are parameters for getIndexInfoOfType. */ +export interface GetIndexInfoOfTypeParams { + snapshot: number; + project: string; + type: number; + keyType: number; +} + /** IndexInfoResponse represents a single index signature. */ export interface IndexInfoResponse { keyType: TypeResponse; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 074471e86cfbb..573a092b1f492 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -1809,6 +1809,22 @@ export class Checker { return type.getIndexInfos(); } + getIndexInfoOfType(type: Type, keyType: Type): IndexInfo | undefined { + const data = this.client.apiRequest("getIndexInfoOfType", { + snapshot: this.snapshotId, + project: this.project.id, + type: type.id, + keyType: keyType.id, + }); + if (!data) return undefined; + return { + keyType: this.objectRegistry.getOrCreateType(data.keyType), + valueType: this.objectRegistry.getOrCreateType(data.valueType), + isReadonly: data.isReadonly ?? false, + declaration: data.declaration ? new NodeHandle(data.declaration, this.project) : undefined, + }; + } + /** * Get the constraint of a type parameter (the `T` in ``), or * undefined if it has none. diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index fe08206bc486b..043dbfb16e4db 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -3914,6 +3914,90 @@ export declare const m: ReadonlyMap; }); }); +describe("Checker - getIndexInfoOfType", () => { + test("returns the index info for a matching key type", async () => { + const src = ` +export interface StringMap { + [key: string]: number; +} +export declare const m: StringMap; +`; + 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("m: StringMap")); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + const stringType = await project.checker.getStringType(); + const info = await project.checker.getIndexInfoOfType(type, stringType); + assert.ok(info); + assert.ok(info.keyType.flags & TypeFlags.String, `Expected string key, got flags ${info.keyType.flags}`); + assert.ok(info.valueType.flags & TypeFlags.Number, `Expected number value, got flags ${info.valueType.flags}`); + assert.equal(info.isReadonly, false); + } + finally { + await api.close(); + } + }); + + test("returns undefined when the type has no matching index", 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 stringType = await project.checker.getStringType(); + const info = await project.checker.getIndexInfoOfType(type, stringType); + assert.equal(info, undefined); + } + finally { + await api.close(); + } + }); + + test("reports isReadonly for a readonly index signature", async () => { + const src = ` +export interface ReadonlyMap { + readonly [key: string]: number; +} +export declare const m: ReadonlyMap; +`; + 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("m: ReadonlyMap")); + assert.ok(symbol); + const type = await project.checker.getTypeOfSymbol(symbol); + const stringType = await project.checker.getStringType(); + const info = await project.checker.getIndexInfoOfType(type, stringType); + assert.ok(info); + assert.equal(info.isReadonly, true); + } + finally { + await api.close(); + } + }); +}); + describe("Checker - getConstraintOfTypeParameter", () => { test("returns constraint of a type parameter", async () => { const api = spawnAPI({ diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 6974061a4a6fb..432aceccc0faf 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -3922,6 +3922,90 @@ export declare const m: ReadonlyMap; }); }); +describe("Checker - getIndexInfoOfType", () => { + test("returns the index info for a matching key type", () => { + const src = ` +export interface StringMap { + [key: string]: number; +} +export declare const m: StringMap; +`; + 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("m: StringMap")); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + const stringType = project.checker.getStringType(); + const info = project.checker.getIndexInfoOfType(type, stringType); + assert.ok(info); + assert.ok(info.keyType.flags & TypeFlags.String, `Expected string key, got flags ${info.keyType.flags}`); + assert.ok(info.valueType.flags & TypeFlags.Number, `Expected number value, got flags ${info.valueType.flags}`); + assert.equal(info.isReadonly, false); + } + finally { + api.close(); + } + }); + + test("returns undefined when the type has no matching index", () => { + 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 stringType = project.checker.getStringType(); + const info = project.checker.getIndexInfoOfType(type, stringType); + assert.equal(info, undefined); + } + finally { + api.close(); + } + }); + + test("reports isReadonly for a readonly index signature", () => { + const src = ` +export interface ReadonlyMap { + readonly [key: string]: number; +} +export declare const m: ReadonlyMap; +`; + 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("m: ReadonlyMap")); + assert.ok(symbol); + const type = project.checker.getTypeOfSymbol(symbol); + const stringType = project.checker.getStringType(); + const info = project.checker.getIndexInfoOfType(type, stringType); + assert.ok(info); + assert.equal(info.isReadonly, true); + } + finally { + api.close(); + } + }); +}); + describe("Checker - getConstraintOfTypeParameter", () => { test("returns constraint of a type parameter", () => { const api = spawnAPI({ diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 45793d96a61be..37cff6406eb9a 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" + MethodGetIndexInfoOfType Method = "getIndexInfoOfType" 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], + MethodGetIndexInfoOfType: unmarshallerFor[GetIndexInfoOfTypeParams], MethodGetIndexInfosOfType: unmarshallerFor[CheckerTypeParams], MethodGetConstraintOfTypeParameter: unmarshallerFor[GetTypePropertyParams], MethodGetBaseConstraintOfType: unmarshallerFor[CheckerTypeParams], @@ -1339,6 +1341,14 @@ type GetPropertyOfTypeParams struct { Name string `json:"name"` } +// GetIndexInfoOfTypeParams are parameters for getIndexInfoOfType. +type GetIndexInfoOfTypeParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + Type TypeID `json:"type"` + KeyType TypeID `json:"keyType"` +} + // GetMemberInModuleExportsParams are parameters for getMemberInModuleExports. type GetMemberInModuleExportsParams struct { Snapshot SnapshotID `json:"snapshot"` diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index d5c668385b849..f9362d0fd1ce8 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(MethodGetIndexInfoOfType): + return s.handleGetIndexInfoOfType(ctx, parsed.(*GetIndexInfoOfTypeParams)) case string(MethodGetIndexInfosOfType): return s.handleGetIndexInfosOfType(ctx, parsed.(*CheckerTypeParams)) case string(MethodGetConstraintOfTypeParameter): @@ -3114,6 +3116,41 @@ func (s *Session) handleGetIndexInfosOfType(ctx context.Context, params *Checker return results, nil } +// handleGetIndexInfoOfType returns the index info for a key type, or nil if none. +// @gen-proto-nullable +func (s *Session) handleGetIndexInfoOfType(ctx context.Context, params *GetIndexInfoOfTypeParams) (*IndexInfoResponse, 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 + } + + keyType, err := setup.resolveTypeHandle(params.KeyType) + if err != nil { + return nil, err + } + + info := setup.checker.GetIndexInfoOfType(t, keyType) + if info == nil { + return nil, nil + } + + result := &IndexInfoResponse{ + KeyType: *setup.newTypeResponse(info.KeyType()), + ValueType: *setup.newTypeResponse(info.ValueType()), + IsReadonly: info.IsReadonly(), + } + if info.Declaration() != nil { + result.Declaration = setup.sd.nodeHandleFrom(info.Declaration()) + } + return result, nil +} + // handleGetConstraintOfTypeParameter returns the constraint of a type parameter. // @gen-proto-nullable func (s *Session) handleGetConstraintOfTypeParameter(ctx context.Context, params *GetTypePropertyParams) (*TypeResponse, error) {