Ran Lottem
Ran Lottem

Reputation: 486

Typescript - Some omit implementations remove optionality from properties

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:

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

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

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

Related Questions