Reputation: 486
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:
Omit
is not the correct oneOmit
does not preserve the 'optionality' of properties (i.e. is not homomorphic), and is therefore different than the real implementation, which does preserve 'optionality'.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'.
Upvotes: 1
Views: 466
Reputation: 249606
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!
Upvotes: 1