From 186a13be8493aa56c03b10cdd8ff1038d7306dd6 Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 11 Sep 2026 12:00:52 +0300 Subject: [PATCH 1/3] Properly serialize `+Infinity` and `-Infinity` number literal type values in API --- packages/typescript/src/api/async/api.ts | 16 +++++++++++- packages/typescript/src/api/sync/api.ts | 16 +++++++++++- packages/typescript/test/async/api.test.ts | 30 ++++++++++++++++++++++ packages/typescript/test/sync/api.test.ts | 30 ++++++++++++++++++++++ tsc/internal/api/proto.go | 6 +++++ 5 files changed, 96 insertions(+), 2 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 2eac81365aae9..6a79623d868f3 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -2544,7 +2544,21 @@ class TypeObject implements Type { // BigInt literal values are serialized as decimal strings (e.g. "-123") because // JSON cannot represent bigint. Decode them back into a real bigint here. const value = data.value as string | number | boolean; - this.value = (data.flags & TypeFlags.BigIntLiteral) ? BigInt(value as string) : value; + if (data.flags & TypeFlags.BigIntLiteral) { + this.value = BigInt(value as string); + } + // JSON cannot represent infinities, so the API serializes them as strings. + else if (data.flags & TypeFlags.NumberLiteral && typeof value === "string") { + if (value === "+Infinity") { + this.value = Infinity; + } + else { + this.value = -Infinity; + } + } + else { + this.value = value; + } } if (data.intrinsicName !== undefined) this.intrinsicName = data.intrinsicName; if (data.isThisType !== undefined) this.isThisType = data.isThisType; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 642a41740c3f2..2ed56ed171479 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -5468,7 +5468,21 @@ class TypeObject implements Type { // BigInt literal values are serialized as decimal strings (e.g. "-123") because // JSON cannot represent bigint. Decode them back into a real bigint here. const value = data.value as string | number | boolean; - this.value = (data.flags & TypeFlags.BigIntLiteral) ? BigInt(value as string) : value; + if (data.flags & TypeFlags.BigIntLiteral) { + this.value = BigInt(value as string); + } + // JSON cannot represent infinities, so the API serializes them as strings. + else if (data.flags & TypeFlags.NumberLiteral && typeof value === "string") { + if (value === "+Infinity") { + this.value = Infinity; + } + else { + this.value = -Infinity; + } + } + else { + this.value = value; + } } if (data.intrinsicName !== undefined) this.intrinsicName = data.intrinsicName; if (data.isThisType !== undefined) this.isThisType = data.isThisType; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 9d0d352061ea2..51ef6d9b59285 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -66,6 +66,7 @@ import { type LiteralType, ModifierFlags, ModuleKind, + type NumberLiteralType, ObjectFlags, type Signature, SignatureKind, @@ -5451,6 +5452,35 @@ describe("FreshableType - getFreshType and getRegularType", () => { assert.equal(negLiteral.value, -123n); }); + test("NumberLiteralType.value is infinity (positive and negative)", async () => { + const src = `\nexport const pos = 1e999;\nexport const neg = -1e999;\n`; + await using api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + + const posSymbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("pos =")); + assert.ok(posSymbol); + const posType = await project.checker.getTypeOfSymbol(posSymbol); + assert.ok(posType); + assert.ok(posType.flags & TypeFlags.NumberLiteral, "Expected NumberLiteral"); + const posLiteral = posType as NumberLiteralType; + assert.equal(typeof posLiteral.value, "number"); + assert.equal(posLiteral.value, Infinity); + + const negSymbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("neg =")); + assert.ok(negSymbol); + const negType = await project.checker.getTypeOfSymbol(negSymbol); + assert.ok(negType); + assert.ok(negType.flags & TypeFlags.NumberLiteral, "Expected NumberLiteral"); + const negLiteral = negType as NumberLiteralType; + assert.equal(typeof negLiteral.value, "number"); + assert.equal(negLiteral.value, -Infinity); + }); + test("getFreshType() returns a fresh twin with matching value", async () => { const src = `\nexport const greeting: "hello" = "hello";\n`; await using api = spawnAPI({ diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index b8c648a5ec343..c50193e66525e 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -81,6 +81,7 @@ import { type LiteralType, ModifierFlags, ModuleKind, + type NumberLiteralType, ObjectFlags, type Signature, SignatureKind, @@ -5319,6 +5320,35 @@ describe("FreshableType - getFreshType and getRegularType", () => { assert.equal(negLiteral.value, -123n); }); + test("NumberLiteralType.value is infinity (positive and negative)", () => { + const src = `\nexport const pos = 1e999;\nexport const neg = -1e999;\n`; + using api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + + const posSymbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("pos =")); + assert.ok(posSymbol); + const posType = project.checker.getTypeOfSymbol(posSymbol); + assert.ok(posType); + assert.ok(posType.flags & TypeFlags.NumberLiteral, "Expected NumberLiteral"); + const posLiteral = posType as NumberLiteralType; + assert.equal(typeof posLiteral.value, "number"); + assert.equal(posLiteral.value, Infinity); + + const negSymbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("neg =")); + assert.ok(negSymbol); + const negType = project.checker.getTypeOfSymbol(negSymbol); + assert.ok(negType); + assert.ok(negType.flags & TypeFlags.NumberLiteral, "Expected NumberLiteral"); + const negLiteral = negType as NumberLiteralType; + assert.equal(typeof negLiteral.value, "number"); + assert.equal(negLiteral.value, -Infinity); + }); + test("getFreshType() returns a fresh twin with matching value", () => { const src = `\nexport const greeting: "hello" = "hello";\n`; using api = spawnAPI({ diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 107fbc580057e..89506902276ed 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -1056,6 +1056,12 @@ func literalValueToJSON(value any) any { case string: return v case jsnum.Number: + if v.IsInf() { + if v > 0 { + return "+Infinity" + } + return "-Infinity" + } return float64(v) case bool: return v From 4ec85e49d0e76755e70ff478823672f8b4af1046 Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 11 Sep 2026 12:46:29 +0300 Subject: [PATCH 2/3] getConstantValue --- packages/typescript/src/api/async/api.ts | 11 ++++++++- .../typescript/src/api/proto.generated.ts | 7 +++++- packages/typescript/src/api/sync/api.ts | 22 ++++++++++++++++-- packages/typescript/test/async/api.test.ts | 23 +++++++++++++++++++ packages/typescript/test/sync/api.test.ts | 23 +++++++++++++++++++ tsc/internal/api/proto.go | 5 ++++ tsc/internal/api/session.go | 9 ++++++-- 7 files changed, 94 insertions(+), 6 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 6a79623d868f3..cd5d66dc8bd70 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -2101,7 +2101,16 @@ export class Checker { project: this.project.id, location: getNodeId(node), }); - return typeof data === "string" || typeof data === "number" ? data : undefined; + if (!data || (typeof data.value !== "string" && typeof data.value !== "number")) { + return undefined; + } + if (data.isNumber && typeof data.value === "string") { + if (data.value === "+Infinity") { + return Infinity; + } + return -Infinity; + } + return data.value; } /** Get the signature of a function-like declaration. Always returns a signature. */ diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index b207862c5705b..1434eaee7d059 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -111,7 +111,7 @@ export interface APIMethodInfo { getImportAdderEdits: APIMethod; getTrueTypeOfConditionalType: APIMethod; getFalseTypeOfConditionalType: APIMethod; - getConstantValue: APIMethod; + getConstantValue: APIMethod; getSignatureFromDeclaration: APIMethod; getExportSpecifierLocalTargetSymbol: APIMethod; getAliasedSymbol: APIMethod; @@ -720,6 +720,11 @@ export interface CheckerNodeParams { location: string; } +export interface ConstantValueResponse { + isNumber: boolean; + value: unknown; +} + /** CheckerSymbolParams are parameters for checker methods that operate on a symbol. */ export interface CheckerSymbolParams { snapshot: number; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 2ed56ed171479..cc75b39b3c5d0 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -4568,7 +4568,16 @@ export class Checker { project: owner.project.id, location: getNodeId(node), }); - return typeof data === "string" || typeof data === "number" ? data : undefined; + if (!data || (typeof data.value !== "string" && typeof data.value !== "number")) { + return undefined; + } + if (data.isNumber && typeof data.value === "string") { + if (data.value === "+Infinity") { + return Infinity; + } + return -Infinity; + } + return data.value; }, function* (node: Node): Generator { const data = yield* apiRequest("getConstantValue", { @@ -4576,7 +4585,16 @@ export class Checker { project: owner.project.id, location: getNodeId(node), }); - return typeof data === "string" || typeof data === "number" ? data : undefined; + if (!data || (typeof data.value !== "string" && typeof data.value !== "number")) { + return undefined; + } + if (data.isNumber && typeof data.value === "string") { + if (data.value === "+Infinity") { + return Infinity; + } + return -Infinity; + } + return data.value; }, ); } diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 51ef6d9b59285..05a2c08ee7880 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -4948,6 +4948,29 @@ describe("Checker - getConstantValue", () => { assert.equal(value, 2); }); + test("returns infinite numeric enum values without changing equivalent strings", async () => { + await using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export enum E { Positive = 1e999, Negative = -1e999, PositiveString = "+Infinity", NegativeString = "-Infinity" }`, + }); + + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sourceFile = await project.program.getSourceFile("/src/main.ts"); + assert.ok(sourceFile); + const members: Node[] = []; + sourceFile.forEachChild(function visit(node) { + if (node.kind === SyntaxKind.EnumMember) members.push(node); + node.forEachChild(visit); + }); + assert.equal(members.length, 4); + + assert.equal(await project.checker.getConstantValue(members[0]), Infinity); + assert.equal(await project.checker.getConstantValue(members[1]), -Infinity); + assert.equal(await project.checker.getConstantValue(members[2]), "+Infinity"); + assert.equal(await project.checker.getConstantValue(members[3]), "-Infinity"); + }); + test("returns string value of a string-initialized enum member", async () => { await using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index c50193e66525e..f403f2b8786af 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -4816,6 +4816,29 @@ describe("Checker - getConstantValue", () => { assert.equal(value, 2); }); + test("returns infinite numeric enum values without changing equivalent strings", () => { + using api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": `export enum E { Positive = 1e999, Negative = -1e999, PositiveString = "+Infinity", NegativeString = "-Infinity" }`, + }); + + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sourceFile = project.program.getSourceFile("/src/main.ts"); + assert.ok(sourceFile); + const members: Node[] = []; + sourceFile.forEachChild(function visit(node) { + if (node.kind === SyntaxKind.EnumMember) members.push(node); + node.forEachChild(visit); + }); + assert.equal(members.length, 4); + + assert.equal(project.checker.getConstantValue(members[0]), Infinity); + assert.equal(project.checker.getConstantValue(members[1]), -Infinity); + assert.equal(project.checker.getConstantValue(members[2]), "+Infinity"); + assert.equal(project.checker.getConstantValue(members[3]), "-Infinity"); + }); + test("returns string value of a string-initialized enum member", () => { using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 89506902276ed..056b61406622f 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -1074,6 +1074,11 @@ func literalValueToJSON(value any) any { } } +type ConstantValueResponse struct { + IsNumber bool `json:"isNumber"` + Value any `json:"value"` +} + type SignatureResponse struct { Id SignatureID `json:"id"` Flags uint32 `json:"flags"` diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index de5b7c7d5ddd6..5fabc63a001c7 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -23,6 +23,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/format" "github.com/microsoft/TypeScript/tsc/internal/ipc" + "github.com/microsoft/TypeScript/tsc/internal/jsnum" "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/ls" "github.com/microsoft/TypeScript/tsc/internal/ls/autoimport" @@ -3549,7 +3550,7 @@ func (s *Session) handleGetPropertyOfType(ctx context.Context, params *GetProper // 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) { +func (s *Session) handleGetConstantValue(ctx context.Context, params *CheckerNodeParams) (*ConstantValueResponse, error) { setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) if err != nil { return nil, err @@ -3564,7 +3565,11 @@ func (s *Session) handleGetConstantValue(ctx context.Context, params *CheckerNod return nil, nil } - return literalValueToJSON(setup.checker.GetConstantValue(node)), nil + result := &ConstantValueResponse{} + value := setup.checker.GetConstantValue(node) + _, result.IsNumber = value.(jsnum.Number) + result.Value = literalValueToJSON(value) + return result, nil } // handleGetSignatureFromDeclaration returns the signature of a function-like declaration. From e3e0f7deb070b3605dc347abb9b501112d7b9a32 Mon Sep 17 00:00:00 2001 From: auvred Date: Sat, 12 Sep 2026 11:27:57 +0300 Subject: [PATCH 3/3] format --- packages/typescript/test/async/api.test.ts | 2 +- packages/typescript/test/sync/api.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 4e8457304b97d..6db186205ccec 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -66,8 +66,8 @@ import { type LiteralType, ModifierFlags, ModuleKind, - type NumberLiteralType, ModuleResolutionKind, + type NumberLiteralType, ObjectFlags, type Signature, SignatureKind, diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index a8a363eff7deb..1b7406a016dca 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -81,8 +81,8 @@ import { type LiteralType, ModifierFlags, ModuleKind, - type NumberLiteralType, ModuleResolutionKind, + type NumberLiteralType, ObjectFlags, type Signature, SignatureKind,