Skip to content
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

String literal generic not always inferred when part of union in nested object #19632

Closed
kpdonn opened this issue Nov 1, 2017 · 7 comments
Closed
Labels
Duplicate An existing issue was already created

Comments

@kpdonn
Copy link
Contributor

kpdonn commented Nov 1, 2017

TypeScript Version: Version 2.7.0-dev.20171031

Code

declare function direct<A extends string>(a: A | A[]): Record<A, string>
declare function nested<A extends string>(a: { fields: A }): Record<A, string>
declare function nestedUnion<A extends string>(a: { fields: A | A[] }): Record<A, string>

const directUnionSingle = direct("z")
const directUnionArray = direct(["z", "y"])
const nestedSingle = nested({fields: "z"})
const nestedUnionSingle = nestedUnion({fields: "z"})
const nestedUnionArray = nestedUnion({fields: ["z", "y"]})

declare function hasZField(arg: { z: string }): void

hasZField(directUnionSingle) // ok
hasZField(directUnionArray) // ok
hasZField(nestedSingle) // ok
hasZField(nestedUnionSingle) // error TS2345: Argument of type 'Record<string, string>' is not assignable to parameter of type '{ z: string; }'
hasZField(nestedUnionArray) // ok

Expected behavior:
All 5 calls to hasZField compile without errors

Actual behavior:
One call does not compile because it infers A to be string instead of "z"

Not a big deal just a quirk I came across and assume is a bug.

@ghost
Copy link

ghost commented Nov 1, 2017

Looks similar to #19497 -- we get this a lot...

@mhegazy mhegazy added the Duplicate An existing issue was already created label Nov 1, 2017
@kpdonn
Copy link
Contributor Author

kpdonn commented Nov 1, 2017

I read through #19497 and I think I understand what's going on in that one but I don't think this is a duplicate. In #19497 as I understand it the issue is essentially about any time a pattern like

declare function arr<T>(val: T): Array<T> // or any type similar to Container<T>

occurs, T is widened from a literal to string.

In my mind that seems mostly unrelated to what is happening in my example. What seems so odd about my example and really makes me think it is a bug is the how the inference works correctly here:

const directUnionSingle = direct("z") // inferred to be Record<"z", string>
const directUnionArray = direct(["z", "y"]) // inferred to be Record<"z" | "y", string>

but that same pattern fails, but only partially fails, if they are nested inside of an object:

const nestedUnionSingle = nestedUnion({fields: "z"}) // inferred to be Record<string, string>
const nestedUnionArray = nestedUnion({fields: ["z", "y"]}) // inferred to be Record<"z" | "y", string>

@kpdonn
Copy link
Contributor Author

kpdonn commented Nov 1, 2017

@andy-ms Also more evidence that this is not a duplicate of #19497 is that all the examples in #19497 work if the generic type is constrained to extend string:

interface Observable<T extends string> {peek(): T}
declare function obs<T extends string>(val: T): Observable<T>
const t: Observable<'A'> = obs('A') // now works
declare function arr<T extends string>(val: T): T[]
const y: "a"[] = arr("a"); // now works

In my example the generic includes extends string so I'd expect it to work.

@ghost
Copy link

ghost commented Nov 1, 2017

To narrow it down, the error is basically that this doesn't work:

declare function f<A extends string>(a: { fields: A | A[] }): Record<A, string>
const r: Record<"z", string> = f({ fields: "z" });

Still seems like another case of #19497 and has the same workaround of "z" as "z".

@kpdonn
Copy link
Contributor Author

kpdonn commented Nov 1, 2017

"z" as "z" is a workaround for the error just like in #19497 but the error itself is definitely not because of the same design limitation that is described there. Expanding on your example from #19497:

declare function arr<T>(val: T): T[]
const y: "a"[] = arr("a"); // error as expected

declare function arr2<T>(val: T | T[]): T[]
const y2: "a"[] = arr2("a"); // error as expected

declare function arr3<T>(val: {fields: T}): T[]
const y3: "a"[] = arr3({fields: "a"}); // error as expected

declare function arr4<T>(val: {fields: T | T[]}): T[]
const y4: "a"[] = arr4({fields: "a"}); // error as expected

declare function arr5<T>(val: {fields: T | T[]}): T[]
const y5: "a"[] = arr5({fields: ["a"]}); // error as expected


declare function strarr<T extends string>(val: T): T[]
const s1: "a"[] = strarr("a"); // no error as expected

declare function strarr2<T extends string>(val: T | T[]): T[]
const s2: "a"[] = strarr2("a"); // no error as expected

declare function strarr3<T extends string>(val: {fields: T}): T[]
const s3: "a"[] = strarr3({fields: "a"}); // no error as expected

declare function strarr4<T extends string>(val: {fields: T | T[]}): T[]
const s4: "a"[] = strarr4({fields: "a"}); // *** unexpected error ***

declare function strarr5<T extends string>(val: {fields: T | T[]}): T[]
const s5: "a"[] = strarr5({fields: ["a"]}); // no error as expected

If it is the same design limitation as #19497 then why do 4 of the 5 start working after adding extends string?

kpdonn added a commit to kpdonn/TypeScript that referenced this issue Nov 2, 2017
Fixes microsoft#19632. The issue was isLiteralContextualType was only checking
for type parameters that extended a literal base type at the top level.
It was already recursively checking the constituents of unions and
intersections but it did so using maybeTypeOfKind which only compared
type flags matching literal or index.

Also adds a test case that failed before this change and succeeds now.
@kpdonn
Copy link
Contributor Author

kpdonn commented Nov 2, 2017

Heads up I added a PR at #19684 that would fix this behavior if accepted.

weswigham added a commit to weswigham/TypeScript that referenced this issue Nov 3, 2017
@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
3 participants