Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I make a `Partial<T>` but only for nullable fields?

Tags:

typescript

I need a Partial<T> like mapped type that allows me make nullable fields optional. I'm working on typing our ORM and it coerces undefined to nulls on nullable fields.

I'd like to take a type of

interface User {
  email: string
  name: string | null
}

And make

interface User {
  email: string
  name?: string | null
}

I tried

type NullablePartial<T> = { [P in keyof T]: T[P] extends null ? T[P] | undefined : T[P] }

But extends null doesn't work for the union of null and another type (and I'd ideally like it to work with more than strings). And the difference between optional fields and undefined values is important.

What can I do?

like image 472
reconbot Avatar asked Dec 14 '22 12:12

reconbot


2 Answers

Making a type function turn just some properties optional or non-optional is actually quite annoying in TypeScript, because there's no simple way to describe it. The easiest way I can think to do it is using multiple mapped types with an intersection. Here's something that should work as long as the type you pass in doesn't have an index signature (it gets more complicated):

type NullablePartial<
  T,
  NK extends keyof T = { [K in keyof T]: null extends T[K] ? K : never }[keyof T],
  NP = Partial<Pick<T, NK>> & Pick<T, Exclude<keyof T, NK>>
> = { [K in keyof NP]: NP[K] }

Note that I'm using Pick and Exclude to carve up T into the nullable properties and the non-nullable properties, doing different things to each of them, then intersecting them back together. Determining nullable properties has to do with checking if null extends T[K] (i.e., "can I assign null to this property"), not the reverse (i.e., "can I assign this property to a variable of type null").

I'm also using generic parameter defaults to make the type manipulation more concise (so I only have to determine the nullable property key names once as NP) and to make the final output type more aesthetically pleasing (by doing one final mapped type at the end I get a type that is not represented as an intersection).

Let's try it on:

interface User {
  email: string
  name: string | null
}

type NPUser = NullablePartial<User>;
// type NPUser = {
//  name?: string | null | undefined;
//  email: string;
// }

Looks reasonable to me... is that close to what you wanted? Good luck.

like image 157
jcalz Avatar answered Feb 04 '23 00:02

jcalz


Let's start by finding which properties are nullable.

type NullablePropertyOf<T> = {
  [K in keyof T]: null extends T[K]
    ? K
    : never
}[keyof T]

We need to modify nullable properties, but leave the rest unaffected. In order to ignore the non-nullable ones in our transformation, we will need the well-known Omit helper.

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

Our final product is created by merging the weak, partial version of the entire T with the properties that were supposed to remain unaffected.

type NullablePartial<T> = Partial<T> & Omit<T, NullablePropertyOf<T>>;

There is, of course, more than one way to do it. In this example, I'm optionalizing everything and requiring what's required. You could take the opposite approach: pick the non-nullable fields first, and optionalize the nullable ones, then merge both types. Whatever suits you.

like image 40
Karol Majewski Avatar answered Feb 03 '23 22:02

Karol Majewski