2

I've written a nested Omit type in two different ways, but the one that makes more sense to me gives convoluted type hints while the one that's less intuitive actually looks best. Here goes:

type HasSameNested = {
    a: {
        c: number; d: string
    },
    b: {
        c: string; d: number
    }
}; // flipped C and D types

type Omit2<T, K1 extends keyof T, K2 extends keyof T[K1]> = {
    [P1 in keyof T]: P1 extends K1 ? {
        [P2 in Exclude<keyof T[K1], K2>]: T[P1][P2]
    } : T[P1]
}

type Omit2_2<T, K1 extends keyof T, K2 extends keyof T[K1]> = {
    [P1 in keyof T]: P1 extends K1 ? {
        [P2 in Exclude<keyof T[P1], K2>]: T[P1][P2]
    } : T[P1]
}

type omit_union = Omit2<HasSameNested, 'a' | 'b', 'c'>;
let T6: omit_union;
T6.a; // works, type is { d: string }
T6.b; // works, type is { d: number }

type omit_union_2 = Omit2_2<HasSameNested, 'a' | 'b', 'c'>;
let T7: omit_union_2;
T7.a.d = 'a'; // works but types are ugly:
// a: { [P2 in Exclude<keyof HasSameNested[K1 & "a"], "c">]: HasSameNested[K1 & "a"][P2]; }
// d: d: HasSameNested[K1 & "a"]["d"]

T7.b.d = 4; // works but types are ugly, as above

Assigning the wrong type to T7.b.d or T7.a.d does tell me e.g. string is not assignable to number, but I don't understand why using Exclude<keyof T[P1], K2> gives such convoluted typing even though P1 in keyof T and P1 extends K1, and Exclude<keyof T[K1], K2> gives the correct types.

6
  • 1
    It does seem like the quickinfo/IntelliSense for Omit2_2 is weird... the seemingly unbound type parameter K1 is present, which is surprising. Maybe that part is a bug in Intellisense? Commented May 9, 2019 at 1:00
  • 1
    The fact that an intersection shows up where you never used one in your definition is due to a change in TS3.4 whereby T extends U ? F<T> : V is simplified to F<T & U> if T definitely extends U. The part where the type parameter makes it into IntelliSense is unexpected, though. Commented May 9, 2019 at 1:17
  • 1
    Shorter repro: type Hmm<T extends U, U> = T extends U ? { [K in keyof T]: any } : never; type What = Hmm<{ a: string }, {}> Commented May 9, 2019 at 1:32
  • The intersection type shows up because P1 extends K1? I've read the change but don't grasp the rationale - isn't P1 already specific enough? What extra information is conveyed by P1 & K1 as a type? Commented May 9, 2019 at 16:30
  • 1
    Yeah me too... I guess optional properties do add info Commented May 9, 2019 at 16:33

1 Answer 1

2

This looks like a bug in the compiler to me. I've just filed Microsoft/TypeScript#31326 and will report back with updated information from there. Good luck!

UPDATE, 2019-05-10: This has been marked as bug by one of the language designers. Not sure when it will be fixed, but at least we know you're right to be confused about that type information.

UPDATE, 2019-05-11: As you noticed, this bug is now fixed and has been merged into master. That means if you now try your code with typescript@next you should see the new behavior:

type omit_union_2 = Omit2_2<HasSameNested, 'a' | 'b', 'c'>;
declare let T7: omit_union_2;
T7.a; // works, type is { d: string }
T7.b; // works, type is { d: number }

I don't see a difference between your T6 and T7 types anymore. So, looks like this was entirely caused by that compiler bug, and should go away when TypeScript 3.5 is released on or around May 30, 2019.

Okay, good luck to you!

Sign up to request clarification or add additional context in comments.

3 Comments

This is exciting! Thanks for posting the bug report. Are you sure our examples relate to the same thing? I ask because in my example I could assign the correct types to e.g. T7.a.d, it's just that the type information isn't very clear. In your example it seems like the type is inferred incorrectly altogether.
I was a little surprised that the issue was actually affecting the type system and not just IntelliSense. The fact that an intersection of a runaway type parameter survived into the final type is either your whole problem or a big problem that's hiding other issues you're having. Either way I think it needs to be fixed in the compiler before I could feel confident about any further advice here.
Yes, and pushed to the master branch, so we can test it. Updated the answer with the results (spoiler: looks good now!)

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.