-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Treat array literal contextually typed by homomorphic mapped types as in tuple context #56555
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
weswigham
merged 3 commits into
microsoft:main
from
Andarist:reverse-mapped-type-tuple-context
Dec 13, 2023
Merged
Changes from all commits
Commits
Show all changes
3 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
148 changes: 148 additions & 0 deletions
148
tests/baselines/reference/reverseMappedTupleContext.symbols
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,148 @@ | ||
//// [tests/cases/compiler/reverseMappedTupleContext.ts] //// | ||
|
||
=== reverseMappedTupleContext.ts === | ||
// https://github.com/microsoft/TypeScript/issues/55382 | ||
|
||
declare function test1<T>(arg: { | ||
>test1 : Symbol(test1, Decl(reverseMappedTupleContext.ts, 0, 0)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 2, 23)) | ||
>arg : Symbol(arg, Decl(reverseMappedTupleContext.ts, 2, 26)) | ||
|
||
[K in keyof T]: T[K]; | ||
>K : Symbol(K, Decl(reverseMappedTupleContext.ts, 3, 3)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 2, 23)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 2, 23)) | ||
>K : Symbol(K, Decl(reverseMappedTupleContext.ts, 3, 3)) | ||
|
||
}): T; | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 2, 23)) | ||
|
||
const result1 = test1(["foo", 42]); | ||
>result1 : Symbol(result1, Decl(reverseMappedTupleContext.ts, 5, 5)) | ||
>test1 : Symbol(test1, Decl(reverseMappedTupleContext.ts, 0, 0)) | ||
|
||
declare function test2<T extends readonly unknown[]>(arg: { | ||
>test2 : Symbol(test2, Decl(reverseMappedTupleContext.ts, 5, 35)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 7, 23)) | ||
>arg : Symbol(arg, Decl(reverseMappedTupleContext.ts, 7, 53)) | ||
|
||
[K in keyof T]: T[K]; | ||
>K : Symbol(K, Decl(reverseMappedTupleContext.ts, 8, 3)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 7, 23)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 7, 23)) | ||
>K : Symbol(K, Decl(reverseMappedTupleContext.ts, 8, 3)) | ||
|
||
}): T; | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 7, 23)) | ||
|
||
const result2 = test2(["foo", 42]); | ||
>result2 : Symbol(result2, Decl(reverseMappedTupleContext.ts, 10, 5)) | ||
>test2 : Symbol(test2, Decl(reverseMappedTupleContext.ts, 5, 35)) | ||
|
||
type Schema = Record<string, unknown> | readonly unknown[]; | ||
>Schema : Symbol(Schema, Decl(reverseMappedTupleContext.ts, 10, 35)) | ||
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) | ||
|
||
type Definition<T> = { | ||
>Definition : Symbol(Definition, Decl(reverseMappedTupleContext.ts, 12, 59)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 13, 16)) | ||
|
||
[K in keyof T]: (() => T[K]) | Definition<T[K]>; | ||
>K : Symbol(K, Decl(reverseMappedTupleContext.ts, 14, 3)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 13, 16)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 13, 16)) | ||
>K : Symbol(K, Decl(reverseMappedTupleContext.ts, 14, 3)) | ||
>Definition : Symbol(Definition, Decl(reverseMappedTupleContext.ts, 12, 59)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 13, 16)) | ||
>K : Symbol(K, Decl(reverseMappedTupleContext.ts, 14, 3)) | ||
|
||
}; | ||
declare function create<T extends Schema>(definition: Definition<T>): T; | ||
>create : Symbol(create, Decl(reverseMappedTupleContext.ts, 15, 2)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 16, 24)) | ||
>Schema : Symbol(Schema, Decl(reverseMappedTupleContext.ts, 10, 35)) | ||
>definition : Symbol(definition, Decl(reverseMappedTupleContext.ts, 16, 42)) | ||
>Definition : Symbol(Definition, Decl(reverseMappedTupleContext.ts, 12, 59)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 16, 24)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 16, 24)) | ||
|
||
const created1 = create([() => 1, [() => ""]]); | ||
>created1 : Symbol(created1, Decl(reverseMappedTupleContext.ts, 17, 5)) | ||
>create : Symbol(create, Decl(reverseMappedTupleContext.ts, 15, 2)) | ||
|
||
const created2 = create({ | ||
>created2 : Symbol(created2, Decl(reverseMappedTupleContext.ts, 18, 5)) | ||
>create : Symbol(create, Decl(reverseMappedTupleContext.ts, 15, 2)) | ||
|
||
a: () => 1, | ||
>a : Symbol(a, Decl(reverseMappedTupleContext.ts, 18, 25)) | ||
|
||
b: [() => ""], | ||
>b : Symbol(b, Decl(reverseMappedTupleContext.ts, 19, 13)) | ||
|
||
}); | ||
|
||
interface CompilerOptions { | ||
>CompilerOptions : Symbol(CompilerOptions, Decl(reverseMappedTupleContext.ts, 21, 3)) | ||
|
||
allowUnreachableCode?: boolean; | ||
>allowUnreachableCode : Symbol(CompilerOptions.allowUnreachableCode, Decl(reverseMappedTupleContext.ts, 23, 27)) | ||
|
||
allowUnusedLabels?: boolean; | ||
>allowUnusedLabels : Symbol(CompilerOptions.allowUnusedLabels, Decl(reverseMappedTupleContext.ts, 24, 33)) | ||
|
||
alwaysStrict?: boolean; | ||
>alwaysStrict : Symbol(CompilerOptions.alwaysStrict, Decl(reverseMappedTupleContext.ts, 25, 30)) | ||
} | ||
type KeepLiteralStrings<T extends string[]> = { | ||
>KeepLiteralStrings : Symbol(KeepLiteralStrings, Decl(reverseMappedTupleContext.ts, 27, 1)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 28, 24)) | ||
|
||
[K in keyof T]: T[K]; | ||
>K : Symbol(K, Decl(reverseMappedTupleContext.ts, 29, 3)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 28, 24)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 28, 24)) | ||
>K : Symbol(K, Decl(reverseMappedTupleContext.ts, 29, 3)) | ||
|
||
}; | ||
declare function test4<T extends Record<string, string[]>>(obj: { | ||
>test4 : Symbol(test4, Decl(reverseMappedTupleContext.ts, 30, 2)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 31, 23)) | ||
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) | ||
>obj : Symbol(obj, Decl(reverseMappedTupleContext.ts, 31, 59)) | ||
|
||
[K in keyof T & keyof CompilerOptions]: { | ||
>K : Symbol(K, Decl(reverseMappedTupleContext.ts, 32, 3)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 31, 23)) | ||
>CompilerOptions : Symbol(CompilerOptions, Decl(reverseMappedTupleContext.ts, 21, 3)) | ||
|
||
dependencies: KeepLiteralStrings<T[K]>; | ||
>dependencies : Symbol(dependencies, Decl(reverseMappedTupleContext.ts, 32, 43)) | ||
>KeepLiteralStrings : Symbol(KeepLiteralStrings, Decl(reverseMappedTupleContext.ts, 27, 1)) | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 31, 23)) | ||
>K : Symbol(K, Decl(reverseMappedTupleContext.ts, 32, 3)) | ||
|
||
}; | ||
}): T; | ||
>T : Symbol(T, Decl(reverseMappedTupleContext.ts, 31, 23)) | ||
|
||
const result4 = test4({ | ||
>result4 : Symbol(result4, Decl(reverseMappedTupleContext.ts, 36, 5)) | ||
>test4 : Symbol(test4, Decl(reverseMappedTupleContext.ts, 30, 2)) | ||
|
||
alwaysStrict: { | ||
>alwaysStrict : Symbol(alwaysStrict, Decl(reverseMappedTupleContext.ts, 36, 23)) | ||
|
||
dependencies: ["foo", "bar"], | ||
>dependencies : Symbol(dependencies, Decl(reverseMappedTupleContext.ts, 37, 17)) | ||
|
||
}, | ||
allowUnusedLabels: { | ||
>allowUnusedLabels : Symbol(allowUnusedLabels, Decl(reverseMappedTupleContext.ts, 39, 4)) | ||
|
||
dependencies: ["baz", "qwe"], | ||
>dependencies : Symbol(dependencies, Decl(reverseMappedTupleContext.ts, 40, 22)) | ||
|
||
}, | ||
}); | ||
|
130 changes: 130 additions & 0 deletions
130
tests/baselines/reference/reverseMappedTupleContext.types
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,130 @@ | ||
//// [tests/cases/compiler/reverseMappedTupleContext.ts] //// | ||
|
||
=== reverseMappedTupleContext.ts === | ||
// https://github.com/microsoft/TypeScript/issues/55382 | ||
|
||
declare function test1<T>(arg: { | ||
>test1 : <T>(arg: { [K in keyof T]: T[K]; }) => T | ||
>arg : { [K in keyof T]: T[K]; } | ||
|
||
[K in keyof T]: T[K]; | ||
}): T; | ||
const result1 = test1(["foo", 42]); | ||
>result1 : [string, number] | ||
>test1(["foo", 42]) : [string, number] | ||
>test1 : <T>(arg: { [K in keyof T]: T[K]; }) => T | ||
>["foo", 42] : [string, number] | ||
>"foo" : "foo" | ||
>42 : 42 | ||
|
||
declare function test2<T extends readonly unknown[]>(arg: { | ||
>test2 : <T extends readonly unknown[]>(arg: { [K in keyof T]: T[K]; }) => T | ||
>arg : { [K in keyof T]: T[K]; } | ||
|
||
[K in keyof T]: T[K]; | ||
}): T; | ||
const result2 = test2(["foo", 42]); | ||
>result2 : [string, number] | ||
>test2(["foo", 42]) : [string, number] | ||
>test2 : <T extends readonly unknown[]>(arg: { [K in keyof T]: T[K]; }) => T | ||
>["foo", 42] : [string, number] | ||
>"foo" : "foo" | ||
>42 : 42 | ||
|
||
type Schema = Record<string, unknown> | readonly unknown[]; | ||
>Schema : readonly unknown[] | Record<string, unknown> | ||
|
||
type Definition<T> = { | ||
>Definition : Definition<T> | ||
|
||
[K in keyof T]: (() => T[K]) | Definition<T[K]>; | ||
}; | ||
declare function create<T extends Schema>(definition: Definition<T>): T; | ||
>create : <T extends Schema>(definition: Definition<T>) => T | ||
>definition : Definition<T> | ||
|
||
const created1 = create([() => 1, [() => ""]]); | ||
>created1 : [number, [string]] | ||
>create([() => 1, [() => ""]]) : [number, [string]] | ||
>create : <T extends Schema>(definition: Definition<T>) => T | ||
>[() => 1, [() => ""]] : [() => number, [() => string]] | ||
>() => 1 : () => number | ||
>1 : 1 | ||
>[() => ""] : [() => string] | ||
>() => "" : () => string | ||
>"" : "" | ||
|
||
const created2 = create({ | ||
>created2 : { a: number; b: [string]; } | ||
>create({ a: () => 1, b: [() => ""],}) : { a: number; b: [string]; } | ||
>create : <T extends Schema>(definition: Definition<T>) => T | ||
>{ a: () => 1, b: [() => ""],} : { a: () => number; b: [() => string]; } | ||
|
||
a: () => 1, | ||
>a : () => number | ||
>() => 1 : () => number | ||
>1 : 1 | ||
|
||
b: [() => ""], | ||
>b : [() => string] | ||
>[() => ""] : [() => string] | ||
>() => "" : () => string | ||
>"" : "" | ||
|
||
}); | ||
|
||
interface CompilerOptions { | ||
allowUnreachableCode?: boolean; | ||
>allowUnreachableCode : boolean | undefined | ||
|
||
allowUnusedLabels?: boolean; | ||
>allowUnusedLabels : boolean | undefined | ||
|
||
alwaysStrict?: boolean; | ||
>alwaysStrict : boolean | undefined | ||
} | ||
type KeepLiteralStrings<T extends string[]> = { | ||
>KeepLiteralStrings : KeepLiteralStrings<T> | ||
|
||
[K in keyof T]: T[K]; | ||
}; | ||
declare function test4<T extends Record<string, string[]>>(obj: { | ||
>test4 : <T extends Record<string, string[]>>(obj: { [K in keyof T & keyof CompilerOptions]: { dependencies: KeepLiteralStrings<T[K]>; }; }) => T | ||
>obj : { [K in keyof T & keyof CompilerOptions]: { dependencies: KeepLiteralStrings<T[K]>; }; } | ||
|
||
[K in keyof T & keyof CompilerOptions]: { | ||
dependencies: KeepLiteralStrings<T[K]>; | ||
>dependencies : KeepLiteralStrings<T[K]> | ||
|
||
}; | ||
}): T; | ||
const result4 = test4({ | ||
>result4 : { alwaysStrict: ["foo", "bar"]; allowUnusedLabels: ["baz", "qwe"]; } | ||
>test4({ alwaysStrict: { dependencies: ["foo", "bar"], }, allowUnusedLabels: { dependencies: ["baz", "qwe"], },}) : { alwaysStrict: ["foo", "bar"]; allowUnusedLabels: ["baz", "qwe"]; } | ||
>test4 : <T extends Record<string, string[]>>(obj: { [K in keyof T & keyof CompilerOptions]: { dependencies: KeepLiteralStrings<T[K]>; }; }) => T | ||
>{ alwaysStrict: { dependencies: ["foo", "bar"], }, allowUnusedLabels: { dependencies: ["baz", "qwe"], },} : { alwaysStrict: { dependencies: ["foo", "bar"]; }; allowUnusedLabels: { dependencies: ["baz", "qwe"]; }; } | ||
|
||
alwaysStrict: { | ||
>alwaysStrict : { dependencies: ["foo", "bar"]; } | ||
>{ dependencies: ["foo", "bar"], } : { dependencies: ["foo", "bar"]; } | ||
|
||
dependencies: ["foo", "bar"], | ||
>dependencies : ["foo", "bar"] | ||
>["foo", "bar"] : ["foo", "bar"] | ||
>"foo" : "foo" | ||
>"bar" : "bar" | ||
|
||
}, | ||
allowUnusedLabels: { | ||
>allowUnusedLabels : { dependencies: ["baz", "qwe"]; } | ||
>{ dependencies: ["baz", "qwe"], } : { dependencies: ["baz", "qwe"]; } | ||
|
||
dependencies: ["baz", "qwe"], | ||
>dependencies : ["baz", "qwe"] | ||
>["baz", "qwe"] : ["baz", "qwe"] | ||
>"baz" : "baz" | ||
>"qwe" : "qwe" | ||
|
||
}, | ||
}); | ||
|
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// @strict: true | ||
// @noEmit: true | ||
|
||
// https://github.com/microsoft/TypeScript/issues/55382 | ||
|
||
declare function test1<T>(arg: { | ||
[K in keyof T]: T[K]; | ||
}): T; | ||
const result1 = test1(["foo", 42]); | ||
|
||
declare function test2<T extends readonly unknown[]>(arg: { | ||
[K in keyof T]: T[K]; | ||
}): T; | ||
const result2 = test2(["foo", 42]); | ||
|
||
type Schema = Record<string, unknown> | readonly unknown[]; | ||
type Definition<T> = { | ||
[K in keyof T]: (() => T[K]) | Definition<T[K]>; | ||
}; | ||
declare function create<T extends Schema>(definition: Definition<T>): T; | ||
const created1 = create([() => 1, [() => ""]]); | ||
const created2 = create({ | ||
a: () => 1, | ||
b: [() => ""], | ||
}); | ||
|
||
interface CompilerOptions { | ||
allowUnreachableCode?: boolean; | ||
allowUnusedLabels?: boolean; | ||
alwaysStrict?: boolean; | ||
} | ||
type KeepLiteralStrings<T extends string[]> = { | ||
[K in keyof T]: T[K]; | ||
}; | ||
declare function test4<T extends Record<string, string[]>>(obj: { | ||
[K in keyof T & keyof CompilerOptions]: { | ||
dependencies: KeepLiteralStrings<T[K]>; | ||
}; | ||
}): T; | ||
const result4 = test4({ | ||
alwaysStrict: { | ||
dependencies: ["foo", "bar"], | ||
}, | ||
allowUnusedLabels: { | ||
dependencies: ["baz", "qwe"], | ||
}, | ||
}); |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO this alone is worth this improvement. Today we can infer tuples using mapped types but we need to awkwardly spread them (TS playground):
That makes the recursive scenarios much more complicated because we can't "just" iterate over the recursive type parameter and have that tuple context assigned to array literals.
IMHO, the mapped type itself strongly indicates that the user wants to have the tuples inferred as they might want to type each element separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn’t infer the tuple type if
T
is markedconst
? I thought that was the whole reason behind const type params, because people wanted to infer tuples instead of(T | U | V)[]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does (although not always, see this TS playground, but that is being fixed by #55794
I don't think that was the whole reason behind it. The reason was that people want to use schema-like arguments for which often strings, numbers, etc have to be kept as literal strings. The fact that tuples are also preserved is just a byproduct of this - since a tuple is a more specific type than an array, just like a string literal is a more specific type than a string type.
Regardless... there are times when constness of the whole argument is not something that I want and preserving the "tuple context" is all that I want. I might want to infer
[string]
and not let's say["foo"]
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, now that I think about this, there’s no precedent for that in the type system today, is there? When inferring arrays in situ you either write
[ "foo", "bar" ]
and getstring[]
or else add anas const
and getreadonly [ "foo", "bar" ]
.So the upshot is that this change would add a way to infer
[ string, string ]
from an array literal via a generic and people will start writing more constrained identity functions to accomplish that (…and likely complain about having to do so 😜)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can already do this today - you have to use the workaround mentioned above. This PR makes this whole thing simpler while also making it possible (or just way easier) to write recursive types that preserve tuples like this (especially in the "mixed" scenarios when your type is supposed to iterate through both object and array types).
I also think that the workaround above isn't that intuitive. After all an unconstrained generic mapped type when iterating over a tuple preserves its tupleness.I would also conceptually consider all array literal expressions to start as tuples~. It's the widening behavior that usually widens them to arrays unless some other hint that the tuple should be preserved is given. So I just expect that contextual mapped type to be considered as such a hint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That’s fair. To be honest it just feels wrong that generic inference seems to have more knobs than what you can control inline. In general, the fewer CIFs I have to write to get type inference to do what I want, the better.