1

The following code compiles:

type Ugh = {
  boo: {[s: string]: Ugh },
  baz: ({ [P in keyof Ugh["boo"]]: Ugh["boo"][P] extends Ugh ? Ugh["boo"][P] : string })
};

const q: Ugh = { boo: {}, baz: {}};
const v: Ugh = {boo: { why: { boo: {}, baz: {}}}, baz: { why: { boo: {}, baz: {}} }};

whereas the following doesn't

type Ugh = {
  boo: {[s: string]: string | Ugh },
  baz: ({ [P in keyof Ugh["boo"]]: Ugh["boo"][P] extends Ugh ? Ugh["boo"][P] : string })
};

const q: Ugh = { boo: {}, baz: {}};
const v: Ugh = {boo: { why: { boo: {}, baz: {}}}, baz: { why: { boo: {}, baz: {}} }};

The only difference is in the type of boo. Why doesn't the second one compile?

2
  • The first one is equivalent to type Ugh = { boo: { [s: string]: Ugh; }; baz: { [x: string]: Ugh; }; }; concrete conditional types will be eagerly evaluated. If you can explain your use case somewhere maybe we can suggest a type that works for what you're trying to do. It is likely that you will have to make Ugh generic. Commented Jun 18, 2019 at 14:16
  • Now that I think about it, it's interesting that the title of this is "recursive generics" when there are no generics in your code. Commented Jun 18, 2019 at 14:35

1 Answer 1

1

Why doesn't the second one compile?

The type string | Ugh does not extend the type Ugh, so in your second example, Ugh["boo"][P] extends Ugh will always be false, with the result that baz will always be of type string.

Here it is in code comments (and in the playground):

type Ugh = {
    // The string index type of Ugh...
    boo: {
        [s: string]: string
    },
    baz: (
        {
            // means that P will always be an Ugh...
            // which does extend Ugh...
            [P in keyof Ugh["boo"]]: Ugh["boo"][P] extends Ugh
            // and so this will always resolve to an Ugh.
            ? Ugh["boo"][P]
            : string
        }
    )
};

type t1 = Ugh["boo"][string] extends Ugh ? true : false; // true

type UghToo = {
    // The string index type of string | UghToo...
    boo: { [s: string]: string | UghToo },
    baz: ({
        // means that P will always be a string | UghToo...
        // which does not extend UghToo...
        [P in keyof UghToo["boo"]]: UghToo["boo"][P] extends UghToo
        ? UghToo["boo"][P]
        // and so this will always resolve to a string.
        : string
    })
};

type t2 = UghToo["boo"][string] extends UghToo ? true : false; // false

Generally, the union of two types (with few exceptions) does not extend either of those types.

type t3 = Date extends Date ? true : false; // true
type t4 = string | Date extends Date ? true : false; // false
type t5 = string | Date extends string ? true : false; // false
Sign up to request clarification or add additional context in comments.

2 Comments

Awesome catch! Is there any way to accomplish the goal (discriminating based on type) without the union? Thanks for your help!
There might be a way to achieve your goal without the union; I suggest putting that follow up question into a new StackOverflow question, because that will keep this question's scope tight, and will also increase the chance of getting a answer to your follow up question.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.