Repository navigation
Treat optional members an index signature covers as keys, erase 'as never', and keep fromEntries objects asserted to records #1496
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
6 changes: 6 additions & 0 deletions
6
...s7/baselines/order-parity/tests/corpus/6465-optional-members-over-index-signature.ts.json
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "order": [ | ||
| "<repo>/tests/corpus/6465-optional-members-over-index-signature.ts" | ||
| ], | ||
| "diags": [] | ||
| } |
6 changes: 6 additions & 0 deletions
6
...mpiler/test/ts7/baselines/order-parity/tests/corpus/6466-never-assertion-argument.ts.json
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "order": [ | ||
| "<repo>/tests/corpus/6466-never-assertion-argument.ts" | ||
| ], | ||
| "diags": [] | ||
| } |
6 changes: 6 additions & 0 deletions
6
...er/test/ts7/baselines/order-parity/tests/corpus/6467-fromentries-record-assertion.ts.json
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "order": [ | ||
| "<repo>/tests/corpus/6467-fromentries-record-assertion.ts" | ||
| ], | ||
| "diags": [] | ||
| } |
6 changes: 6 additions & 0 deletions
6
...ler/test/ts7/baselines/order-parity/tests/diagnostics/fromentries-aliased-entries.ts.json
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "order": [ | ||
| "<repo>/tests/diagnostics/fromentries-aliased-entries.ts" | ||
| ], | ||
| "diags": [] | ||
| } |
73 changes: 73 additions & 0 deletions
73
tests/corpus/6465-optional-members-over-index-signature.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| // An index signature whose value admits undefined already describes an | ||
| // optional member of the same type (`interface Env extends Dict<string> | ||
| // { TZ?: string }`, @types/node's ProcessEnv): such a record is one key | ||
| // set in insertion order, so spread, delete, `in` and every enumeration | ||
| // treat the declared member like any other key. | ||
| interface Env { | ||
| TZ?: string; | ||
| [key: string]: string | undefined; | ||
| } | ||
|
|
||
| const base: Env = { A: "1", TZ: "UTC", B: "2" }; | ||
| const env = { ...base }; | ||
| env.C = "3"; | ||
| for (const key of ["A", "TZ", "MISSING"]) { | ||
| delete env[key]; | ||
| } | ||
| console.log(Object.entries(env), Object.keys(base)); | ||
| const probe = (key: string): boolean => key in env; | ||
| console.log(probe("TZ"), probe("B"), env.TZ, env.B); | ||
| env.TZ = "Europe/Berlin"; | ||
| console.log(JSON.stringify(env), env); | ||
|
|
||
| const extended: Env = { ...base, D: "4" }; | ||
| delete extended.TZ; | ||
| extended.TZ = "Asia/Tokyo"; | ||
| console.log(extended, Object.keys(extended).join(",")); | ||
|
|
||
| // A declared member written after another key comes after it. | ||
| interface Scores { | ||
| best?: number; | ||
| [name: string]: number | undefined; | ||
| } | ||
| const scores: Scores = {}; | ||
| scores["x"] = 1; | ||
| scores.best = 2; | ||
| console.log(Object.keys(scores), JSON.stringify(scores)); | ||
|
|
||
| // The process environment has the same shape; a child environment built | ||
| // from it keeps the parent's keys in order. | ||
| process.env.SCX_FIRST = "first"; | ||
| process.env.SCX_DROP = "drop"; | ||
| process.env.SCX_LAST = "last"; | ||
| const DROP = ["SCX_DROP", "SCX_NOT_SET"]; | ||
| function childEnv(): string[] { | ||
| const runEnv = { ...process.env }; | ||
| runEnv.SCX_ADDED = "added"; | ||
| for (const key of DROP) { | ||
| delete runEnv[key]; | ||
| } | ||
| runEnv.SCX_FIRST = runEnv.SCX_FIRST ? `${runEnv.SCX_FIRST}!` : "none"; | ||
| return Object.entries(runEnv) | ||
| .filter(([key]) => key.startsWith("SCX_")) | ||
| .map(([key, value]) => `${key}=${value || ""}`); | ||
| } | ||
| console.log(childEnv()); | ||
|
|
||
| // A record of declared optional members over a slot without undefined is | ||
| // the same key set: merging it over the process environment keeps both. | ||
| type Dictionary<T> = { [key: string]: T }; | ||
| type BuildEnv = { CI?: string; DEBUG?: string } & Dictionary<string>; | ||
| function merged(buildEnv: BuildEnv): string[] { | ||
| const env = { ...process.env, ...buildEnv }; | ||
| return [ | ||
| String(env.CI === "1" || env.SCX_FIRST === "first"), | ||
| `${env.DEBUG ?? "-"}`, | ||
| Object.keys(env) | ||
| .filter((key) => key.startsWith("SCX_") || key === "CI" || key === "DEBUG") | ||
| .join(","), | ||
| ]; | ||
| } | ||
| const buildEnv: BuildEnv = { SCX_BUILD: "b", CI: "1" }; | ||
| buildEnv.DEBUG = "yes"; | ||
| console.log(merged(buildEnv), Object.keys(buildEnv), buildEnv.CI, buildEnv["SCX_BUILD"]); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| // `value as never` makes an unknown value fit any parameter: the argument | ||
| // is checked against the parameter's type like any unknown value the | ||
| // checker admits there, and a typed value passes unchanged. | ||
| type Out = { routes: { src: string; dest?: string }[]; count: number }; | ||
|
|
||
| function show(tags: Record<string, unknown>): void { | ||
| console.log(JSON.stringify(tags)); | ||
| } | ||
|
|
||
| function report(tag: (result: Out) => Record<string, unknown>, value: unknown): void { | ||
| show(tag(value as never)); | ||
| } | ||
|
|
||
| const describe = (r: Out): Record<string, unknown> => ({ count: r.count, first: r.routes[0]?.src }); | ||
| report(describe, JSON.parse('{"routes":[{"src":"/a"},{"src":"/b","dest":"/c"}],"count":2}')); | ||
|
|
||
| // Through a promise of unknown, the way a wrapper tags an async result. | ||
| function later(value: Out): Promise<unknown> { | ||
| return Promise.resolve(value); | ||
| } | ||
| later({ routes: [], count: 7 }).then((value) => { | ||
| show(describe(value as never)); | ||
| }); | ||
|
|
||
| // A typed value keeps its identity through the assertion. | ||
| function bump(target: Out): void { | ||
| target.count++; | ||
| } | ||
| const typed: Out = { routes: [{ src: "/x" }], count: 1 }; | ||
| bump(typed as never); | ||
| console.log(typed.count); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // `Object.fromEntries(...) as T` for a declared record T keeps exactly | ||
| // the keys the entries created, in their order, as Node keeps the object | ||
| // itself, as long as the entries hold scalars or checked-dynamic values. | ||
| type Tags = { "error.type": string; "error.msg": string; "error.cause"?: string }; | ||
|
|
||
| function scrub(input: string): string { | ||
| return input.replace(/secret/g, "***"); | ||
| } | ||
|
|
||
| function redact(tags: Tags): Tags { | ||
| return Object.fromEntries( | ||
| Object.entries(tags).map(([key, value]) => [key, typeof value === "string" ? scrub(value) : value]), | ||
| ) as Tags; | ||
| } | ||
|
|
||
| const e = new Error("a secret here"); | ||
| const plain = redact({ "error.type": e.name, "error.msg": e.message }); | ||
| console.log(plain, Object.keys(plain), "error.cause" in plain, plain["error.msg"].length); | ||
| const caused = redact({ "error.type": "TypeError", "error.msg": "m", "error.cause": "secret cause" }); | ||
| console.log(caused, JSON.stringify(caused)); | ||
|
|
||
| // Keys the record does not declare stay with the object. | ||
| type Point = { x: number; y: number }; | ||
| const p = Object.fromEntries([ | ||
| ["y", 2], | ||
| ["x", 1], | ||
| ["z", 3], | ||
| ]) as Point; | ||
| console.log(p, p.x + p.y, Object.keys(p), JSON.stringify(p)); | ||
|
|
||
| // Entries held as unknown keep their identity: the object's members are | ||
| // the same array and record, and writes through either reach both. | ||
| type Holder = { list: number[]; point: { x: number } }; | ||
| const list = [1, 2]; | ||
| const point = { x: 1 }; | ||
| const entries: [string, unknown][] = [ | ||
| ["list", list], | ||
| ["point", point], | ||
| ]; | ||
| const held = Object.fromEntries(entries) as Holder; | ||
| held.list.push(3); | ||
| held.point.x = 5; | ||
| console.log(list.length, point.x, held.list === list, held.point === point); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| // `Object.fromEntries(...) as T` keeps the object only when the entries' | ||
| // values are scalars or checked-dynamic values. Native arrays and records | ||
| // in the entries would be copied, severing their identity, so the | ||
| // assertion keeps the exact-shape fence (corpus 6467 covers the cases that | ||
| // lower). | ||
| type Scores = { a: number[]; b: number[] }; | ||
| const shared = [10]; | ||
| const scores = Object.fromEntries([ | ||
| ["a", shared], | ||
| ["b", shared], | ||
| ]) as Scores; | ||
| scores.a.push(11); | ||
| console.log(shared.length, scores.b.length); |
8 changes: 8 additions & 0 deletions
8
tests/harness/__snapshots__/fromentries-aliased-entries.ts.txt
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| fromentries-aliased-entries.ts:8:16 - error SC2002: record shapes must match exactly or width-coerce: expected '{ a: number[]; b: number[] }', got '{ [key: string]: number[] }' — 'a' is not a declared field of the source, and the source's index signature could hold it at runtime (a completed undefined would drop that value) | ||
|
|
||
| 7 | const shared = [10]; | ||
| 8 | const scores = Object.fromEntries([ | ||
| | ^~~~~~~~~~~~~~~~~~~~ | ||
| 9 | ["a", shared], | ||
|
|
||
| hint: width subtyping compiles as a copy (each expected field copied off the source; extra fields drop) — this pair is outside the copy's rules; build a literal with exactly the expected fields instead |
1 change: 1 addition & 0 deletions
1
tests/harness/corpus-costs/6465-optional-members-over-index-signature.ts.json
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| { "plain": 3225, "san": 26787 } |
1 change: 1 addition & 0 deletions
1
tests/harness/corpus-costs/6466-never-assertion-argument.ts.json
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| { "plain": 2895, "san": 23565 } |
1 change: 1 addition & 0 deletions
1
tests/harness/corpus-costs/6467-fromentries-record-assertion.ts.json
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| { "plain": 3093, "san": 22400 } |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.