Typescript Partial But No Undefined
I believe Partial
is one of the most common utility types.
It is convenient, it allows you to skip properties
For example, to update specific fields in the database
type abc= Partial<{a:number, b:string, c:boolean}>const updateDB=(data:abc)=>{}updateDB({a:1, c:true})
however, Partial also union all the properties with undefined
updateDB({a:undefined, b:undefined, c:undefined})
This could be problematic because not everyone wants undefined
, some databases cannot accept undefined
(for example Firestore)
How can we solve this? How can we make properties optional and at the same time prevent undefined
from polluting database?
We can solve this by using generic and mapped types
type abc= {a:number, b:string, c:boolean}type PartialButNoUndefined <Type extends Record<string,unknown>,Data extends Record<string,unknown>> =
keyof Data extends keyof Type
? {
[K in keyof Data]:Data[K] extends Type[K]?Data[K]:`type of property ${K & string} is incorrect`
}
:`unknown property:${Exclude<keyof Data, keyof Type> & string}`
type allProps = PartialButNoUndefined<abc,{a:1,b:"b",c:true}> // {a: 1, b: "b",c: boolean }
type partialProps = PartialButNoUndefined<abc,{b:"b"}> // { b:string }
type propWithIncorrectValue = PartialButNoUndefined<abc,{a:1, b:true}> // {a,:1, b:"type of property b is incorrect" }
type unknownProps = PartialButNoUndefined<abc,{a:1, z:5, u:true}> // "unknown property:z" | "unknown property:u"
What PartialButNoUndefined
does:
- detect unknown properties
- detect incorrect properties type
As you can see, now all members are optional without the need to union with undefined
.
the last step is to implement PartialButNoUndefined
into our updateDB
function
const updateDB=<T extends Record<string,unknown>>(data:PartialButNoUndefined<abc,T>)=>{}
ok, let’s try it out!
updateDB({a: 1, b: "b",c: true }) // expect ok
updateDB({b:"b"}) // expect ok
updateDB({c:undefined}) // expect error
hmm, something not right…why?
This is because Typescript unable to infer T
from thedata
argument
recall how we normally infer type from argument
const updateDB=<T extends Record<string,unknown>>(data:T)=>{}
We assign the naked type parameter as the data argument type, and this is the reason generic inference works (now this is not accurate, generic inference also works with non-naked type parameter, but you can understand it as it is now, I will open another post explaining it in details)
but now we have
const updateDB=<T extends Record<string,unknown>>(data:PartialButNoUndefined<abc,T>)=>{}
but now we assign non-naked type parameter PartialButNoUndefined<abc,T>
as data argument type
so how are we going to solve this?
now recall the condition for generic inference to work, that is
assign the naked type parameter as the data argument type
and we are going to do that
const updateDB=<T extends Record<string,unknown>>(data:T extends never? T: PartialButNoUndefined<abc,T>)=>{}
and boom, it works!
but HOW? What is happening?
Let’s review:
- Now we are clear that we need to assign naked parameter type
T
to thedata
argument in order for generic inference to work - But we don’t want to use generic
T
asdata
type, we wantPartialButNoUndefined<abc,T>
ourdata
type - so what
data: T extends never? T: PartialButNoUndefined<abc,T>
do is telling Typescript: "heh you need to infer the type of thedata
argument from user input as T, but the type of data has to bePartialButNoUndefined<abc,T>
"
This is possible, because T
will not extends never
and condition will return PartialButNoUndefined<abc,T>
as type.
That is it, now we can create optional types without having to worry about undefined
.
finally let us simplify the first, we can drop the descriptive types because we can rely on type checking now.
type abc= {a:number, b:string, c:boolean}type PartialButNoUndefined <Type extends Record<string,unknown>,Data extends Record<string,unknown>> =
{
[K in keyof Data]: Type[K extends keyof Type ? K:never]
}const updateDB=<T extends Record<string,unknown>>(data:T extends never? T: PartialButNoUndefined<abc,T>)=>{}updateDB({a: 1, b: "b",c: true }) // ok
updateDB({b:"b"}) // ok
updateDB({a:true, b:"abc",c:undefined}) // expect error
updateDB({b:"abc",z:1}) // expect error
this looks cleaner and has more concise type checking
now here is the interesting thing, if you remove T extends never? T:
type abc= {a:number, b:string, c:boolean}type PartialButNoUndefined <Type extends Record<string,unknown>, Data extends Record<string,unknown>> =
{
[K in keyof Data]: Type[K extends keyof Type ? K:never]
}const updateDB=<T extends Record<string,unknown>>(data: PartialButNoUndefined<abc,T>)=>{}updateDB({a: 1, b: "b",c: true }) // ok
updateDB({b:"b"}) // ok
updateDB({a:true, b:"abc",c:undefined}) // expect error
updateDB({b:"abc",z:1}) // expect error
It still works
But why it doesn’t work previously and need to rely on T extends never? T:
?
Unfortunately I have no idea, such behavior is not explained in the doc, but maybe explained in some patch note which I have no idea which one
Feel free to comment if you know the explanation
update: today we have simpler way to do stuff: exactOptionalPropertyTypes