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

Mapped Object Type with Conditional Types - Takes Neither #26931

Closed
ShanonJackson opened this issue Sep 6, 2018 · 7 comments
Closed

Mapped Object Type with Conditional Types - Takes Neither #26931

ShanonJackson opened this issue Sep 6, 2018 · 7 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@ShanonJackson
Copy link

TypeScript Version: 3.0.3

Search Terms: - Mapped Conditional Types, Mapped Types Bug

Code

class Foo {
    name = "foo";
}
class Bar {
    name = "bar";
}
type Test = {
    foo: Foo;
    bar: Bar;
}
// doesn't matter what type "any" here is, it takes neither.
type TrueOrFalse<T> = T extends any ? true : false;
const getFromTest = <K extends keyof Test>(key: K) => {
    const TestType1: TrueOrFalse<Test[K]> = true; // fails
    const TestType2: TrueOrFalse<Test[K]> = false;  // fails
    const TestType3: TrueOrFalse<Test[K]> = "" as any as true & false // fails
    const TestType4: TrueOrFalse<Test[K]> = "" as any as true | false // fails
}

Expected behavior:
TrueOrFalse<Test[K]> should be either true or false but comes out as neither. regardless of the type of "any" in the type alias.

Actual behavior:
It chooses neither of the conditional types.

Playground Link:
https://agentcooper.github.io/typescript-play/#code/MYGwhgzhAEBiD29oG8CwAoaXoDswFsBTaAXmgCIAzRcgbgwF8NRIYAhMAJxQ21wOJlyAIy51GGAC4BPAA7EAKoQiTSPTNmrwAXHET0NWUZ10dOBpugD0V6ABN4ynAHJV+MJMmFuAdwAWHtAy8hRgONLk0H7exACWEAA00LGqkmAA1sq4hCnRnAB0UnKKnACuhADynLBgIBCEADwKAHxqCtCEAB5eOHYwYdLQAPxBZcS6lLX1BsDwOCrQAOaEkrCc8PhKC2QNANId3YS9MJnS8JTQW5LNABSnursAlKStaIbQs-OqVwrFAIy6BRjKo1OqNK4AbV2AF1WmRJGNaNAbNBJrE6rxsJ8Fj9igAmQHA6pTcHKSRQ2FqSZgpHI2xojHvbHfMm-eQAZkJ5RBJKaZIpcIokUg0AGopgCPK0AAZKiSXS5eiIJisMzLqzigAWLmVYlgvkqAVqcjC-rhcWjKUAHzlYIVDOV6EsQA

@mattmccutchen
Copy link
Contributor

mattmccutchen commented Sep 6, 2018

This is working (mostly) as intended. TrueOrFalse<T> is a distributive conditional type, so TrueOrFalse<Test[K]> doesn't get simplified because we don't know whether Test[K] might turn out to be a union that needs to be distributed. That's the general rule; it might seem silly in this case because anything is assignable to any, but I don't think it's worth making an exception. The only thing that's likely to change here is that true & false should be assignable to TrueOrFalse<Test[K]>; I've just filed #26933 for that. (As jack-williams pointed out there, the rule should not apply to distributive conditional types.) (Oh, and we do know the documentation is poor. ☹️ )

Anyway, you probably want a non-distributive conditional type:

type TrueOrFalse<T> = [T] extends [any] ? true : false;

@ShanonJackson
Copy link
Author

ShanonJackson commented Sep 6, 2018

Hello @mattmccutchen,
Not too familiar with non-distributive condition types however how can i fix this for my use case which is.....
I have the following type in a self-controlled private repository.

at<K extends keyof C>(path: C extends Array<any> ? number : K): DoesntMatterType

Basically if C is array take a number to index it, else take a key of C to index it.
This type has worked perfectly for my whole project up untill this single point where i have some type T indexed by a single type K (K extends keyof T)
such that i have... T[K]
when i try to "at" something on my T[K] i can pass neither a keyof T[K] nor a number.

How can i fix the typings to continue to work for everything else but also to solve this case.

EDIT: TL;DR
I have T[K] and the function above "at"
how can i change "at" to continue to work when "C" is T[K].

Also, it also makes sense that T[K] should be the index type of T, therefore shouldn't the conditional type choose a branch based on the index type of T[K] ? If someone could explain that to me that would also help my understanding.

@ahejlsberg
Copy link
Member

@ShanonJackson Why isn't your at method just declared as the following?

at<K extends keyof C>(path: K): DoesntMatterType

After all, when C is an array type, keyof C does indeed include number (plus the names of the properties of the array, but you might actually want to access "length" so that seems ok).

There is currently no way your at method will work on a higher order type such as a T[K]. It is simply beyond the type system's capability to abstractly reason about the type C extends any[] ? number : K. Even if we did have a rule that allowed assigning to an unresolved conditional type, the source type in your case would need to be (keyof T[K]) & number in order to be assignable to both branches.

@ShanonJackson
Copy link
Author

ShanonJackson commented Sep 6, 2018

Bingo, back when i wrote that i think keyof T was just resolving to string (i think).
Thank you. Resolved.

EDIT: It would be nice for some documentation on how non-distributive conditional types work and why.
I'm working with alot of index typing and it seems like every step i just run into new issues working with T[K] and not T[keyof T].

Can't spread T[K] (spread types may only be created from object types.
Can't conditional branch T[K] easily (still need to learn how to)
Can't use array methods on T[K] and infer the correct return type without casting.

@ahejlsberg ahejlsberg added the Question An issue which isn't directly actionable in code label Sep 6, 2018
@ahejlsberg
Copy link
Member

@ShanonJackson You can read about distributive conditional types in the original PR #21316.

@ShanonJackson
Copy link
Author

ShanonJackson commented Sep 7, 2018

Would you be able to explain these to me.....
can't see them there in the documentation.

interface Foo {
    name: "foo";
    siblings: number[]
}
interface Bar {
    name: "bar";
    siblings: string[]
}

type Test = {
    foo: Foo;
    bar: Bar;
}

const getFromTest = <K extends keyof Test>(key: K) => {
    const t = ("" as any as Test)[key]
    const yolo = {...t}  // not spreadable? why.
    const test = t.siblings.filter((x) => "" === x.name) // cannot invoke .....
    // why shouldn't X become number & string? or number | string
}

Ontop of these 2 i have 2-3 more issues that's making the typing of certain functions impossible in my project, also related to the T[K] problem, but it's going to invole bigger repro steps

@mattmccutchen
Copy link
Contributor

@ShanonJackson Please take this to Stack Overflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

3 participants