Typescript Partial But No Undefined

Acid Coder
4 min readJun 30, 2022

--

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})

playground

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"

playground

What PartialButNoUndefined does:

  1. detect unknown properties
  2. 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

playground

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>)=>{}

playground

and boom, it works!

but HOW? What is happening?

Let’s review:

  1. Now we are clear that we need to assign naked parameter type T to the data argument in order for generic inference to work
  2. But we don’t want to use generic T as data type, we want PartialButNoUndefined<abc,T> our data type
  3. 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 be PartialButNoUndefined<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

playground

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

playground

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

--

--

Acid Coder
Acid Coder

Written by Acid Coder

Typescript Zombie. Youtube Pikachu On Acid. (Unrelated to programming but by watching it you become a good developer overnight)