From 8d3d1c910626565b81df09f4079b5effd56f34a5 Mon Sep 17 00:00:00 2001 From: mrazauskas Date: Sat, 22 Aug 2026 09:05:51 +0300 Subject: [PATCH] Add `.getNonMissingTypeOfSymbol()` method Signed-off-by: mrazauskas --- packages/typescript/src/api/async/api.ts | 15 +++++ .../typescript/src/api/proto.generated.ts | 1 + packages/typescript/src/api/sync/api.ts | 15 +++++ packages/typescript/test/async/api.test.ts | 56 +++++++++++++++++++ packages/typescript/test/sync/api.test.ts | 56 +++++++++++++++++++ tsc/internal/api/proto.go | 2 + tsc/internal/api/session.go | 18 ++++++ tsc/internal/checker/exports.go | 4 ++ 8 files changed, 167 insertions(+) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index ad787fb62495d..3b88e6cccc488 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1394,6 +1394,21 @@ export class Checker { return this.objectRegistry.getOrCreateType(data); } + /** + * Get the type of a symbol, excluding the missing type when + * `exactOptionalPropertyTypes: true` is set; for symbols whose + * type cannot be determined the checker yields the error type + * (use {@link Type.isErrorType} to detect it). + */ + async getNonMissingTypeOfSymbol(symbol: Symbol): Promise { + const data = await this.client.apiRequest("getNonMissingTypeOfSymbol", { + snapshot: this.snapshotId, + project: this.project.id, + symbol: symbol.id, + }); + return this.objectRegistry.getOrCreateType(data); + } + async getReferencesToSymbolInFile(file: DocumentIdentifier, symbol: Symbol): Promise { const data = await this.client.apiRequest("getReferencesToSymbolInFile", { snapshot: this.snapshotId, diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 35c08679d2aa4..2b2d2b2ef3e8b 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -32,6 +32,7 @@ export interface APIMethodInfo { getTypeOfSymbol: APIMethod; getTypesOfSymbols: APIMethod; getDeclaredTypeOfSymbol: APIMethod; + getNonMissingTypeOfSymbol: APIMethod; getSourceFile: APIMethod; getSourceFileNames: APIMethod; getSourceFileMetadata: APIMethod; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 074471e86cfbb..3aaa073dabedf 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -1402,6 +1402,21 @@ export class Checker { return this.objectRegistry.getOrCreateType(data); } + /** + * Get the type of a symbol, excluding the missing type when + * `exactOptionalPropertyTypes: true` is set; for symbols whose + * type cannot be determined the checker yields the error type + * (use {@link Type.isErrorType} to detect it). + */ + getNonMissingTypeOfSymbol(symbol: Symbol): Type { + const data = this.client.apiRequest("getNonMissingTypeOfSymbol", { + snapshot: this.snapshotId, + project: this.project.id, + symbol: symbol.id, + }); + return this.objectRegistry.getOrCreateType(data); + } + getReferencesToSymbolInFile(file: DocumentIdentifier, symbol: Symbol): NodeHandle[] { const data = this.client.apiRequest("getReferencesToSymbolInFile", { snapshot: this.snapshotId, diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index fe08206bc486b..d5f08a82ed7ac 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -536,6 +536,62 @@ describe("Snapshot", () => { await api.close(); } }); + + test("getNonMissingTypeOfSymbol", async () => { + const api = spawnAPI({ + "/tsconfig-one.json": JSON.stringify({ + compilerOptions: { + exactOptionalPropertyTypes: true, + strict: true, + }, + }), + "/tsconfig-two.json": JSON.stringify({ + compilerOptions: { + exactOptionalPropertyTypes: false, + strict: true, + }, + }), + "/src/index.ts": "const x: Partial<{ a: string }> = {};", + }); + + try { + // when `"exactOptionalPropertyTypes": true` + const snapshot1 = await api.updateSnapshot({ openProject: "/tsconfig-one.json" }); + const project1 = snapshot1.getProject("/tsconfig-one.json")!; + const type1 = await project1.checker.getTypeAtPosition("/src/index.ts", 7); + assert.ok(type1); + const symbol1 = await project1.checker.getPropertyOfType(type1, "a"); + assert.ok(symbol1); + const propertyType1 = await project1.checker.getTypeOfSymbol(symbol1); + assert.ok(propertyType1); + // getTypeOfSymbol returns 'string | undefined' + assert.ok(propertyType1.isUnionType()); + const propertyType2 = await project1.checker.getNonMissingTypeOfSymbol(symbol1); + assert.ok(propertyType2); + // getNonMissingTypeOfSymbol returns 'string' + assert.ok(!propertyType2.isUnionType()); + assert.ok(propertyType2.flags & TypeFlags.String); + + // when `"exactOptionalPropertyTypes": false` + const snapshot2 = await api.updateSnapshot({ openProject: "/tsconfig-two.json" }); + const project2 = snapshot2.getProject("/tsconfig-two.json")!; + const type2 = await project2.checker.getTypeAtPosition("/src/index.ts", 7); + assert.ok(type2); + const symbol2 = await project2.checker.getPropertyOfType(type2, "a"); + assert.ok(symbol2); + const propertyType3 = await project2.checker.getTypeOfSymbol(symbol2); + assert.ok(propertyType3); + // getTypeOfSymbol returns 'string | undefined' + assert.ok(propertyType3.isUnionType()); + const propertyType4 = await project2.checker.getNonMissingTypeOfSymbol(symbol2); + assert.ok(propertyType4); + // getNonMissingTypeOfSymbol returns 'string | undefined' + assert.ok(propertyType4.isUnionType()); + } + finally { + await api.close(); + } + }); }); describe("LanguageService - imports", () => { diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 6974061a4a6fb..db9e273e83a1f 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -544,6 +544,62 @@ describe("Snapshot", () => { api.close(); } }); + + test("getNonMissingTypeOfSymbol", () => { + const api = spawnAPI({ + "/tsconfig-one.json": JSON.stringify({ + compilerOptions: { + exactOptionalPropertyTypes: true, + strict: true, + }, + }), + "/tsconfig-two.json": JSON.stringify({ + compilerOptions: { + exactOptionalPropertyTypes: false, + strict: true, + }, + }), + "/src/index.ts": "const x: Partial<{ a: string }> = {};", + }); + + try { + // when `"exactOptionalPropertyTypes": true` + const snapshot1 = api.updateSnapshot({ openProject: "/tsconfig-one.json" }); + const project1 = snapshot1.getProject("/tsconfig-one.json")!; + const type1 = project1.checker.getTypeAtPosition("/src/index.ts", 7); + assert.ok(type1); + const symbol1 = project1.checker.getPropertyOfType(type1, "a"); + assert.ok(symbol1); + const propertyType1 = project1.checker.getTypeOfSymbol(symbol1); + assert.ok(propertyType1); + // getTypeOfSymbol returns 'string | undefined' + assert.ok(propertyType1.isUnionType()); + const propertyType2 = project1.checker.getNonMissingTypeOfSymbol(symbol1); + assert.ok(propertyType2); + // getNonMissingTypeOfSymbol returns 'string' + assert.ok(!propertyType2.isUnionType()); + assert.ok(propertyType2.flags & TypeFlags.String); + + // when `"exactOptionalPropertyTypes": false` + const snapshot2 = api.updateSnapshot({ openProject: "/tsconfig-two.json" }); + const project2 = snapshot2.getProject("/tsconfig-two.json")!; + const type2 = project2.checker.getTypeAtPosition("/src/index.ts", 7); + assert.ok(type2); + const symbol2 = project2.checker.getPropertyOfType(type2, "a"); + assert.ok(symbol2); + const propertyType3 = project2.checker.getTypeOfSymbol(symbol2); + assert.ok(propertyType3); + // getTypeOfSymbol returns 'string | undefined' + assert.ok(propertyType3.isUnionType()); + const propertyType4 = project2.checker.getNonMissingTypeOfSymbol(symbol2); + assert.ok(propertyType4); + // getNonMissingTypeOfSymbol returns 'string | undefined' + assert.ok(propertyType4.isUnionType()); + } + finally { + api.close(); + } + }); }); describe("LanguageService - imports", () => { diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 45793d96a61be..3a9fb9603dab8 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -82,6 +82,7 @@ const ( MethodGetTypeOfSymbol Method = "getTypeOfSymbol" MethodGetTypesOfSymbols Method = "getTypesOfSymbols" MethodGetDeclaredTypeOfSymbol Method = "getDeclaredTypeOfSymbol" + MethodGetNonMissingTypeOfSymbol Method = "getNonMissingTypeOfSymbol" MethodGetSourceFile Method = "getSourceFile" MethodGetSourceFileNames Method = "getSourceFileNames" MethodGetSourceFileMetadata Method = "getSourceFileMetadata" @@ -428,6 +429,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetTypeOfSymbol: unmarshallerFor[GetTypeOfSymbolParams], MethodGetTypesOfSymbols: unmarshallerFor[GetTypesOfSymbolsParams], MethodGetDeclaredTypeOfSymbol: unmarshallerFor[GetTypeOfSymbolParams], + MethodGetNonMissingTypeOfSymbol: unmarshallerFor[GetTypeOfSymbolParams], MethodResolveName: unmarshallerFor[ResolveNameParams], MethodGetSymbolsInScope: unmarshallerFor[GetSymbolsInScopeParams], MethodGetSignaturesOfType: unmarshallerFor[GetSignaturesOfTypeParams], diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index d5c668385b849..78382b3dc960c 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -653,6 +653,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetTypesOfSymbols(ctx, parsed.(*GetTypesOfSymbolsParams)) case string(MethodGetDeclaredTypeOfSymbol): return s.handleGetDeclaredTypeOfSymbol(ctx, parsed.(*GetTypeOfSymbolParams)) + case string(MethodGetNonMissingTypeOfSymbol): + return s.handleGetNonMissingTypeOfSymbol(ctx, parsed.(*GetTypeOfSymbolParams)) case string(MethodResolveName): return s.handleResolveName(ctx, parsed.(*ResolveNameParams)) case string(MethodGetSymbolsInScope): @@ -1642,6 +1644,22 @@ func (s *Session) handleGetDeclaredTypeOfSymbol(ctx context.Context, params *Get return setup.newTypeResponse(setup.checker.GetDeclaredTypeOfSymbol(symbol)), nil } +// handleGetNonMissingTypeOfSymbol returns the type of a symbol, excluding the missing type. +func (s *Session) handleGetNonMissingTypeOfSymbol(ctx context.Context, params *GetTypeOfSymbolParams) (*TypeResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + symbol, err := setup.resolveSymbolHandle(params.Symbol) + if err != nil { + return nil, err + } + + return setup.newTypeResponse(setup.checker.GetNonMissingTypeOfSymbol(symbol)), nil +} + // handleResolveName resolves a name to a symbol at a given location. // @gen-proto-nullable func (s *Session) handleResolveName(ctx context.Context, params *ResolveNameParams) (*SymbolResponse, error) { diff --git a/tsc/internal/checker/exports.go b/tsc/internal/checker/exports.go index 49efc3208d430..766638a5d7a9f 100644 --- a/tsc/internal/checker/exports.go +++ b/tsc/internal/checker/exports.go @@ -181,6 +181,10 @@ func (c *Checker) GetTypeOfSymbol(symbol *ast.Symbol) *Type { return c.getTypeOfSymbol(symbol) } +func (c *Checker) GetNonMissingTypeOfSymbol(symbol *ast.Symbol) *Type { + return c.getNonMissingTypeOfSymbol(symbol) +} + func (c *Checker) GetConstraintOfTypeParameter(typeParameter *Type) *Type { return c.getConstraintOfTypeParameter(typeParameter) }