When Omit<Type, Keys> breaks (my expectation)
2022-07-12
I have scratched my head when Omit<Type, Keys> in TypeScript has not worked as I intended.
This blog post will share with you what I have learned from reasoning why Omit has not worked.
What is Omit<Type, Keys>?
Omit<Types, Keys> is a utility type provided by TypeScript.
It defines a new type that has all the properties of Type but omits properties specified to Keys.
We can see it as a type operator that removes Keys from Type.
Suppose you have the following definitions,
;
The derived type Person will be equivalent to
When Omit<Type, Keys> breaks
Suppose I have the types similar to the following defined outside of my project.
I want to make the title property of InfoObject optional.
So I derive a new type from InfoObject by using Omit.
;
Now I can omit the title property to initialize an instance of InfoObjectOmittingTitle.
; // ERROR! error TS2741: Property 'title' is missing in type '{ version: string; }' but required in type 'InfoObject'.
; // WORKS!
But a strange thing happens.
The version property also has become optional contrary to my expectation.
; // WORKS!?
Additionally, even the following can be compiled!
; // WORKS!?
How Omit<Type, Keys> works
You can find the definition of Omit itself here in the source code of TypeScript.
The following is an excerpt of the code.
;
Omit is actually a composition of other utility types Pick and Exclude.
What is Pick<Type, Keys>?
Pick is the opposite of Omit.
It defines a new type that has properties of Type included in Keys.
The definition of Pick itself is here.
The following is an excerpt of the code.
;
What is Exclude<UnionType, ExcludedMembers>?
Exclude defines a new union type that includes all the members of UnionType but excludes members in ExcludedMembers.
The definition of Exclude itself is here.
The following is an excerpt of the code.
;
What is going on in my failed attempt?
We are going to dissect the following part in the previous section.
Omit<InfoObject, 'title'>
This will be
Pick<InfoObject, Exclude<keyof InfoObject, 'title'>>
keyof InfoObject is equivalent to the following union type.
Please refer to this page for how keyof works.
'title' | 'version' | 'description' | string;
string comes from the index signature [extensionName: string]: any of the base interface IElement.
So the Omit part will further become
Pick<InfoObject, Exclude<'title' | 'version' | 'description' | string, 'title'>>
The following table shows the evaluation of Exclude<T, 'title'> for each member T of 'title' | 'version' | 'description' | string.
We have to understand "Distributive Conditional Types" to see what happens to the part Exclude<'title' | 'version' | 'description' | string, 'title'>.
T | T extends 'title' ? | Exclude<T, 'title'> |
|---|---|---|
'title' | true | never |
'version' | false | 'version' |
'description' | false | 'description' |
string | false | string |
Thus the Exclude<'title' | 'version' | 'description' | string, 'title'> will be
'version' | 'description' | string
And the Omit part will become
Pick<InfoObject, 'version' | 'description' | string>
What happens to the final Pick?
Here what I can do is only guessing from the consequence.
I think the most inclusive member string would win at K extends keyof T or P in K during the expansion of Pick<T, K> and suppress other specific members 'version' and 'description'.
And the Pick would end up with
However, I have not confirmed any legitimate literature that backs up my guess yet.
Workaround
According to this article, the following can be a workaround.
;
Partial<Type> defines a new type that has every property of Type as optional.
Intersection with Pick<InfoObject, 'version'> keeps the version property mandatory.
The major drawback is that it requires an exhaustive list of mandatory properties.
Conclusion
Omit<Type, Keys> may not work if Type contains an index signature.
I have not confirmed in the TypeScript specification why this happens.
But I have found a workaround.