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.