Repository navigation
Treat optional members an index signature covers as keys, erase 'as never', and keep fromEntries objects asserted to records - #1496
Merged
Conversation
…ever', and keep fromEntries objects asserted to records
A record type whose declared members are all optional and typed as its
index signature's slot (ProcessEnv's TZ?, '{ CI?: string } &
Dictionary<string>') interns as the pure index-signature record, its
slot admitting undefined: spread, delete and enumeration then follow one
insertion order. 'value as never' erases, so the consuming slot checks
an unknown value like any other. 'Object.fromEntries(...) as T' for a
declared record T goes through the checked-dynamic bridge, which keeps
the entries' keys and order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…s or records Object.fromEntries(...) as T passed the entries through the checked-dynamic bridge, which copies native arrays and records: an array shared by two entries, or held elsewhere, became separate copies, and writes through the asserted object no longer reached it. The bridge now applies only when the entries' values are scalars or checked-dynamic values, which pass by value or by identity; other entries keep the exact-shape fence (SC2002). Corpus 6467 checks that entries held as unknown keep their identity; diagnostics fixture fromentries-aliased-entries.ts pins the fence.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three gaps involving index-signature records.
Optional members an index signature already covers. @types/node declares
interface ProcessEnv extends Dict<string> { TZ?: string }. That madeprocess.envand every spread of it (const env = { ...process.env }) a hybrid record:TZgot a struct slot, and every other key went to the overflow map. The same holds for a declared environment type such as{ CI?: string; DEBUG?: string } & Dictionary<string>. Spreading these records into each other, or deleting their keys, was refused. The declared members also enumerated before the overflow keys, whereas Node keeps a single insertion order. On main,s["x"] = 1; s.best = 2enumeratedbest, xwithout a warning. A record type now interns as the pure index-signature record when its declared members are all optional and typed as its slot, meaning the member type is the slot plus undefined. The slot then admits undefined, as the optional members do. Spread, delete,inwith a runtime key, and every enumeration follow the object's insertion order. Required members, and members typed otherwise than the slot, keep the hybrid shape. The check reads the member type off the members, so a hybrid that does not qualify interns nothing new.value as never.nevermaps to a numeric placeholder, sotag(value as never)checked an unknown value againstnumber. The assertion now erases, and the consuming parameter applies the same validated extraction that any unknown value gets where the checker admits it. A typed value passes through unchanged and keeps its identity.Object.fromEntries(...) as Tfor a declared recordT, as in theredact<T extends Record<string, unknown>>(t: T): Tidiom. The entries build a pure index-signature record, which shares no layout withT, so this used to fail with SC2002. In Node the assertion keeps the object, with the keys and order the entries gave it. fromEntries returns a fresh object nothing else references, so the checked-dynamic bridge is indistinguishable from the original. That bridge keeps keys, extra keys and order, asJSON.parse(text) as Talready does. Sources that may be aliased keep the exact-shape fence.Evidence
6465-optional-members-over-index-signature.ts6466-never-assertion-argument.ts6467-fromentries-record-assertion.ts2556-width-hybrid-shapes.ts(Opts/FewerOpts): executable shrinks by 0.13%.5233-inline-union-fields-shapes.ts(Scores): executable shrinks by 0.02%.Scorescase above).pnpm lintandpnpm format:checkare clean.pnpm test:ts7 --baselines-onlypasses, with the three new baselines recorded.tests/harness/diagnostics.test.tsandtype-mapper.test.tspass.🤖 Generated with Claude Code