DEV Community

Acid Coder
Acid Coder

Posted on • Updated on

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})
Enter fullscreen mode Exit fullscreen mode

however, Partial also union all the properties with undefined

updateDB({a:undefined, b:undefined, c:undefined})
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

playground

What PartialButNoUndefined does:

  1. detect unknown properties
  2. detect incorrect properties type
  3. limit

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>)=>{}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

playground

Partial But no Undefined 1

hmm, something not right...why?

This is because Typescript unable to infer T from data argument

recall how we normally infer type from argument

const updateDB=<T extends Record<string,unknown>>(data:T)=>{}
Enter fullscreen mode Exit fullscreen mode

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>)=>{}
Enter fullscreen mode Exit fullscreen mode

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>)=>{}
Enter fullscreen mode Exit fullscreen mode

playground

Partial But No Undefined2

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 data argument from user input as T, but the type of the 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
Enter fullscreen mode Exit fullscreen mode

playground

partial but no undefined 3

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
Enter fullscreen mode Exit fullscreen mode

partial but no undefined 4

playground

It still works

But why it doesn't work earlier 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 why

update: today we have simpler way to do stuff: exactOptionalPropertyTypes

Top comments (0)