Repository navigation
Fix invalid declaration emit for constants whose value is Infinity or NaN - #64761
Sid Chaudhary (sidcraftscode) wants to merge 1 commit into
Conversation
… NaN A numeric literal that overflows, such as 1e400, has the number literal type Infinity. Declaration emit wrote these constants with their literal value, producing `declare const a = Infinity;`. That is not a valid ambient initializer (TS1254: it must be a string or numeric literal or a literal enum reference), so the emitted .d.ts did not type-check. The same happened for -Infinity, readonly class properties and `export default 1e400`. These declarations now get a `number` type annotation instead. Enum member references whose value is NaN or Infinity are unchanged, since `declare const x = E.A;` is valid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
🟢 Approval recommended
The focused emit fix correctly handles non-finite values and is supported by comprehensive regression baselines.
0 open findings
What changed in this PR
Fixes invalid declaration emit for non-finite numeric literal values by falling back to number annotations.
Changes:
- Rejects
InfinityandNaNas ambient constant initializers. - Preserves valid enum-member references and finite literals.
- Adds comprehensive compiler regression coverage and baselines.
| File | Description |
|---|---|
tsc/internal/checker/emitresolver.go |
Handles non-finite literal values during declaration emit. |
tsc/testdata/tests/cases/compiler/declarationEmitNonFiniteNumberLiteral.ts |
Adds regression scenarios. |
tsc/testdata/baselines/reference/compiler/declarationEmitNonFiniteNumberLiteral.js |
Captures corrected declaration output. |
tsc/testdata/baselines/reference/compiler/declarationEmitNonFiniteNumberLiteral.types |
Records inferred types. |
tsc/testdata/baselines/reference/compiler/declarationEmitNonFiniteNumberLiteral.symbols |
Records symbol information. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
| t := r.checker.getTypeOfSymbol(s) | ||
| if !isFreshLiteralType(t) { | ||
| return false | ||
| } |
There was a problem hiding this comment.
This isn't strictly true. We can actually just emit a fixed overflowing numeric literal instead of Infinity, like 1e1000 and input code functionality would be preserved.
Really, we should formally support Infinity as a lib type alias over the value global of the same name, tho. But then with a formally-recognized Infinity type we have to start talking about freshness for type references and isFinite and NaN and "is NaN a number (yes, in the TS sense, imo)" and "are we narrowing non-finite numbers correctly" and.... really it's just a big can of worms for an edge case most people don't care about, and every time I've brought it up before, our conclusion was "it seems complicated for little payoff, do nothing".
Sorry, tangent - back on this PR. Rather than skipping initializer emit entirely (which is observably different), just emit a large fixed overflow value instead - it's observably identical within TS as far as type arithmetic goes.
There was a problem hiding this comment.
So, like #64110 ?
Fixes #64760
Problem
A numeric literal that overflows, such as
1e400, has the number literal typeInfinity. Declaration emit writes constants of that type with their literal value, which produces declaration files that don't type-check:Each initializer is an error when the
.d.tsis checked:TS1254: A 'const' initializer in an ambient context must be a string or numeric literal or literal enum reference.Infinityis an identifier, not a literal.This isn't a TS 7 regression. TypeScript 6.0.3 (Playground) emits the same initializers, and
declare const _default: Infinity;for the default export.Fix
In
tsc/internal/checker/emitresolver.go:IsLiteralConstDeclarationreturns false when the declaration's type is a non-enum number literal whose value isInfinity,-InfinityorNaN, so the declaration gets a type annotation instead of an initializer.CreateLiteralConstValuereturns nil for those values. Theexport defaultpath calls it directly and already falls back to a type annotation on nil. This replaces the branches that built theInfinityandNaNidentifiers, which produced exactly the invalid initializers above.Output after the fix:
Enum members with
NaNorInfinityvalues are unaffected (declarationEmitEnumNaN.ts). So are references to them:export const g = E.Astill emits= E.A, which is a valid initializer.An alternative would be to reuse the source text (
= 1e400), which keeps the literal type. It needs a fallback anyway, since the type can come from an expression that isn't a numeric literal. I went withnumberto keep the change small. Happy to switch if you prefer.Related: #42905 (Infinity in type positions, fixed in 7.1) and #63831 (
-1e500 as constemitted as-Infinity). The type-position case in #63831 goes through the node builder and isn't touched here. #56272 discusses whether overflowing literals should have anInfinityliteral type at all. This fix doesn't depend on that outcome: either way, the emitted.d.tsmust type-check.Tests
declarationEmitNonFiniteNumberLiteral.ts. It coversconst, negative values, the smallest overflowing literal (1.7976931348623159e308), a large finite literal that must keep its value (1e308),static readonlyandreadonlyproperties,export default, and references to NaN and Infinity enum members. Without the fix, the harness's.d.tsre-check records 6 TS1254 errors (//// [DtsFileErrors]). With it, there are none.declarationEmit*, enum,fakeInfinity1andnanEqualitycompiler test is unchanged (599 tests).go test ./...intscpasses: 64 packages, no failures (golang:1-bookworm in Docker).internal/testrunnerwas run on its own with-parallel 4, because the Docker VM's 4 GB ran o