Typescript Partial But No Undefined
Tags: webdev, programming, tutorial, typescript
Published: 2022-06-30 16:45:00

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
```ts
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`

```ts
updateDB({a:undefined, b:undefined, c:undefined})
```

[playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAhgRgYwLxQAowE7AJYwDYA8A3jAFwB2ArgLZwQYA0UcpAzsBtuQOZMKlwA9oLwQY5AL4A+AFAyEg8uyiUwAExjAIAEQBCSABQbgZeAgCUSKUQlzVxnboMlSARj6kOlCBPN31mo7OZJTkahAAZlwQakwsoeFR5DEeCZHRar5AA)

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

```ts
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](https://www.typescriptlang.org/play?#code/C4TwDgpgBAhgRgYwLxQN4wFwDsCuBbOCAJwBoo4MBnYIgSywHMyEM4B7NgGwhiwF8AUANCQoABRhFgtGJwBCOYADk2AVSwATCADN6EDVAA8AFXDQIAD2ARNlKACUICNkQ2HqdRiRxYA1ljYAdywAPhIAERhgGChLa1sHJxc3D3omH38g0JCoFAEoAqhfCBA2bShI6NirGw07YtLy00h8woB+NFbCwoBtAGkoeiKSsoqomABdDEqYfonq+LqoZog5tpm5jAADEWhRsCI2SCkQKAASVAGAMihUxj5Bu3pnIiInYC2uqEFu7YyA4JQA5HYigDAXACiFgQnBwWkMDVGMzIiKaZhyNzuDD4WygQmEZlgnE4YkOYDsKAkUhk8kUKnUWl0WH0hngCBI6AwAEYSBQAERwPkkFg0HAQPg5AD0krQmCgPPIGCgAqFLHIHG4vG+BNEYEk0lkpKOFPE+ppCmUak0Oj0bjZHP5golUGlaEVWO1uyBZIA6rRgAALACSWBebwQwAAarIxblTdTZBb6damSz7ZyFRQfIzbc7XegSNyyPyvfsyaDTnBHoNQy5w8A+Z7Cf8skbyXGqQbaZaGTbmXbEBzMAqAF4YACsZBwGFF4qlMr5LcBwOOYJHjYAPsql1hvSCThgcHygA)

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
```ts
const updateDB=<T extends Record<string,unknown>>(data:PartialButNoUndefined<abc,T>)=>{}
```
ok, let's try it out!
```ts
updateDB({a: 1, b: "b",c: true }) // expect ok
updateDB({b:"b"}) // expect ok
updateDB({c:undefined}) // expect error
```
[playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAhgRgYwLxQN4wFwDsCuBbOCAJwBoo4MBnYIgSywHMyEM4B7NgGwhiwF8AUANCQoABRhFgtGJwBCOYADk2AVSwATCADN6EDVAA8AFXDQIAD2ARNlKACUICNkQ2HqdRiRxYA1ljYAdywAPhIAERhgGChLa1sHJxc3D3omH38g0JCoFAEoAqhfCBA2bShI6NirGw07YtLy00h8woB+NFbCwoBtAGkoeiKSsoqomABdDEqYfonq+LqoZog5tpm5jAADEWhRsCI2SCkQKAASVAGAMihUxj5Bu3pnIiInYC2uqEFu7YyA4JQA5HYigDAXACiFgQnBwWkMDVGMzIiKaZhyNzuDD4WygQmcWGoUBwYA0UQg4TkSBMC1qdkcLxSNDS3j8AOyAAoydEMBIpDJ5IoVOotLosPpDPAECRjCEAJRIEKoQQCEncilyDnoDBQACMZAoUAARHAjSQWFAaDhoHw5VAAPT26qQBDAKBsXyq0nkylaigmo22h1Oywut0er3q32oFg+UV6DRBx3O96xV4uIA)

![Partial But no Undefined 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdmxrcz0hfglfmo4iai8.png)

hmm, something not right...why?

This is because Typescript unable to infer `T` from `data` argument

recall how we normally infer type from argument

```ts
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
```ts
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

```ts
const updateDB=<T extends Record<string,unknown>>(data:T extends never? T: PartialButNoUndefined<abc,T>)=>{}
```

[playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAhgRgYwLxQN4wFwDsCuBbOCAJwBoo4MBnYIgSywHMyEM4B7NgGwhiwF8AUANCQoABRhFgtGJwBCOYADk2AVSwATCADN6EDVAA8AFXDQIAD2ARNlKACUICNkQ2HqdRiRxYA1ljYAdywAPhIAERhgGChLa1sHJxc3D3omH38g0JCoFAEoAqhfCBA2bShI6NirGw07YtLy00h8woB+NFbCwoBtAGkoeiKSsoqomABdDEqYfonq+LqoZog5tpm5jAADEWhRsCI2SCkQKAASVAGAMihUxj5Bu3pnIiInYC2uqEFu7YyA4JQA5HYigDAXACiFgQnBwWkMDVGMzIiKaZhyNzuDD4WygQmcWGoUBwYA0UQg4TkSBMC1qdkcLxSNDS3j8AOyAAoydEMMZaQksBAAG7EDrGDDiSTSWQKZRqTQ6PRueAIEjGEIASiQIVQggEJO5FLkHPQEoAjGQKFAAERwa0kFhQGg4aB8DVQAD0HqgbF8+tJ5MpJootutbs93t9-sNQdNzoglow1pV9pYPi0ukFGnDXuqkAQwFirxc0cDxtQIZTJAAXhgzTnvZZ84XiIciEA)

![Partial But No Undefined2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nxmltcs0emkwsfomd078.png)
 

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.

```ts
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](https://www.typescriptlang.org/play?#code/C4TwDgpgBAhgRgYwLxQN4wFwDsCuBbOCAJwBoo4MBnYIgSywHMyEM4B7NgGwhiwF8AUANCQoABRhFgtGJwBCOYADk2AVSwATCADN6EDVAA8AFXDQIAD2ARNlKACUICNkQ2HqdRiRxYA1ljYAdywAPhIAERhgGChLa1sHJxc3D3omH38g0JCoFAEoArR8wpKAbQBpKHooXwgQNm0oSOiAXQwoU0gK2KsbDTta+sbO6AB+KHLsCAA3YhbigsECgWcsaigcMA0oiHC5JBMe+P7E51d3GjTvPwDgkJCACm3ojGMjvrssGeJx43aJKQyeSKFTqLS6L5ueAIEjGEIASiQIVQggEm2euzkD3Q7QAjGQKFAAERwIkkFhQGg4aB8eFQAD09KgbF8aK2Oz22IoJKJtIZTJZbIxnJxVIgBIwROhZJYPnBeg0fMZPUgCGAsSIRBcQo5WNQ3OlJAAXhhcUqmZZVeriFqiEIBEA)

![partial but no undefined 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0sl7my2z8g9nxgehxcbs.png)

this looks cleaner and has more concise type checking

now here is the interesting thing, if you remove `T extends never? T:`
```ts
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
```
![partial but no undefined 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kk4gkni90vilhsd3vvs4.png)
 
[playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAhgRgYwLxQN4wFwDsCuBbOCAJwBoo4MBnYIgSywHMyEM4B7NgGwhiwF8AUANCQoABRhFgtGJwBCOYADk2AVSwATCADN6EDVAA8AFXDQIAD2ARNlKACUICNkQ2HqdRiRxYA1ljYAdywAPjIAERhgGChLa1sHJxc3D3omH38g0JCoFAEoArR8wpKAbQBpKHooXwgQNm0oSOiAXQwoU0gK2KsbDTta+sbO6AB+KHLsCAA3YhbigsECgWcsaigcMA0oiHC5JBMe+P7E51d3GjTvPwDgkJCACm3o9okpGXlFFXUtXSx9QzwBAkYwhACUSBCqEEAk2z12cge6HaAEYyBQoAAiOCYkgsKA0HDQPhgqAAejJUDYvlhWx2eyRFGxmJJ5Mp1Np8IZyMJEHRGExQNxLB8vz0GlZFJ6kAQwFiRCILk59MRqCZQpIAC8MCjJZTLDK5cRFUQhAIgA)

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](https://devblogs.microsoft.com/typescript/announcing-typescript-4-4-beta/#exact-optional-property-types)