1

I've written several implementations of Omit, including the one shown by Intellisense when hovering over Omit itself. I'm running into difficulty understanding why some implementations are homomorphic and others are not.

I've found that:

  • The implementation shown when hovering over Omit is not the correct one
  • The implementation shown when hovering over Omit does not preserve the 'optionality' of properties (i.e. is not homomorphic), and is therefore different than the real implementation, which does preserve 'optionality'.
  • Two other implementations I've written are also not-homomorphic, and I can't understand why.

Here's my code:

// a type with optional and readonly properties
type HasOptional = { a: number; b?: number, c: string; d?: readonly string[]; };

// first attempt
type Omit_1<T, K extends keyof T> = { [P in Exclude<keyof T, K>]: T[P]; };
type Omit_1_Optional = Omit_1<HasOptional, 'a'>; // b, d lost optionality

// Omit's 'fake' implementation, as shown by Intellisense
type Omit_2<T, K extends string | number | symbol> = { [P in Exclude<keyof T, K>]: T[P]; };
type Omit_2_Optional = Omit_2<HasOptional, 'a'>; // b, d lost optionality

// Using Omit itself
type Omit_3<T, K extends string | number | symbol> = Omit<T, K>;
type Omit_3_Optional = Omit_3<HasOptional, 'a'>; // optionality maintained!

// Writing Omit's implementation explicitly
type Omit_4<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Omit_4_Optional = Omit_4<HasOptional, 'a'>; // optionality maintained!

I've seen here, in an answer about deep Omit that [P in K]: is used as an extra level of indirection to cause homomorphic behavior, but that's also present here and yet the first two implementations don't preserve 'optionality'.

Ran Lottem
  • 476
  • 5
  • 17

1 Answers1

1

A mapped type is considered homomorphic in two cases. Either we map over keyof T( docs) or we map over a type parameter K where K has a constraint of keyof T (K extends keyof T, docs).

While Exclude<keyof T, K> does extend keyof T is does not fit into these two specific cases. This means that mapping directly over Exclude<keyof T, K> will not produce a homomorphic mapped type. If we take Exclude<keyof T, K> and put it into a type parameter that has the required constraint then we get the desired behavior.

// a type with optional and readonly properties
type HasOptional = { a: number; b?: number, c: string; d?: readonly string[]; };

// mapping over Exclude<keyof T, K> optionality lost
type Omit_1<T, K extends keyof T> = { [P in Exclude<keyof T, K>]: T[P]; };
type Omit_1_Optional = Omit_1<HasOptional, 'a'>; // b, d lost optionality

// mapping over Exclude<keyof T, K> optionality lost
type Omit_2<T, K extends string | number | symbol> = { [P in Exclude<keyof T, K>]: T[P]; };
type Omit_2_Optional = Omit_2<HasOptional, 'a'>; // b, d lost optionality

// Omit in 3.5 has homomorphic behavior since it uses Pick which is  homomorphic 
type Omit_3<T, K extends string | number | symbol> = Omit<T, K>;
type Omit_3_Optional = Omit_3<HasOptional, 'a'>; // optionality maintained!

// has homomorphic behavior since it uses Pick which is  homomorphic
type Omit_4<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Omit_4_Optional = Omit_4<HasOptional, 'a'>; // optionality maintained!
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Thanks for your answer! What I don't quite grasp is _why_ `Exclude`, while extending `keyof T`, doesn't fit these cases. It looks like `Pick` does exactly the same as the first implementation - it maps over the same things, it just specifies that the second generic parameter `extends keyof T`. I can be satisfied however with knowing that `Pick` is homomorphic and to use that. – Ran Lottem May 11 '19 at 20:19
  • Another issue I have with my implementation not working is that while it's not homomorphic, the type hints are explicit, and the ones for `Omit_3` and `Omit_4` are not, because they use `Pick<>...` as part of the description. – Ran Lottem May 11 '19 at 20:24
  • 1
    @RanLottem It's not homomorphic because while it does extend `keyof T` it is not one of those EXACT cases. It's not that we must map over something that extends `keyof T` it must either be `keyof T `or a type parameter with a `keyof T` constraint. nothing else will trigger the homomorphic behavior. – Titian Cernicova-Dragomir May 11 '19 at 20:28
  • 1
    @RanLottem With regard to the tooltip displayed that is a bit of a heuristic the compiler uses. If the type is just an alias for another type it will display the alias (in this case `Pick` if it is a mapped type it will not be expanded. For conditionals it depends but they will usually be resolved and the result displayed .. not all of the rules are 100% clear to me either. – Titian Cernicova-Dragomir May 11 '19 at 20:30