Home > Mobile >  How to make a type in TypeScript which is like a combination between Pick and Partial?
How to make a type in TypeScript which is like a combination between Pick and Partial?

Time:12-21

I would like to have a type where I know specific properties are going to be defined, but some properties are going to be missing. Something like this:

type UserType = {
  email: string
  name: {
    first: string
    last: string
  }
  address: {
    city: string
    state: string
    zip: string
    coordinates: {
      lat: number
      lng: number
    }
  }
}

const partialUser: PickPartial<
  UserType,
    | 'email'
    | Record<
      'address',
      Record<
        'coordinates',
          | 'lat'
          | 'lng'
      >
    >
>

Basically I am trying to select like this:

{
  email: string
  name?: {
    first: string
    last: string
  }
  address: {
    city?: string
    state?: string
    zip?: string
    coordinates: {
      lat: number
      lng: number
    }
  }
}

Is anything like this possible? I saw the PartialDeep code, but that doesn't work the way I want to. I essentially want to say, these specific properties in the tree are defined (not possibly undefined), and the rest are possibly undefined ?. How can I accomplish that in TypeScript? If not exactly, what is as close as I can get to that, or what are some workarounds?

Another API approach might be:

const partialUser: PickPartial<
  UserType,
  {
    email: string
    address: {
      coordinates: {
        lat: number
        lng: number
      }
    }
  }
>

Then everything else gets a ? possibly undefined key. Is it possible for that to be done somehow?

CodePudding user response:

First, I'd be inclined to use a structure like

PickPartial<
  UserType, 
  { email: 1, address: { coordinates: { lat: 1, lng: 1 } } }
>

where the potentially nested key set is represented by a single object, possessing the same nested keys you care about. The nested values aren't really important as long as they are not themselves object types (otherwise their keys would be probed). I chose 1 above because it's short, but you could use string or number or whatever. I'm suggesting this representation because it consistently treats keys as keys and not sometimes bare string literal types.


Anyway, using that, we can write PickPartial<T, K> mostly like

type PickPartial<T, M> = (
  Partial<Omit<T, keyof M>> & 
  { [K in keyof T & keyof M]: 
      M[K] extends object ? PickPartial<T[K], M[K]> : T[K]
  }
);

First, Partial<Omit<T, keyof M>> means that any part of T that doesn't have a key in M (using the Omit<T, K> utility type) will be made partial (using the Partial<T> utility type). Then, { [K in keyof T & keyof M]: M[K] extends object ? PickPartial<T[K], M[K]> : T[K] } maps the keys of T which are also in M and either recursively PickPartial's them (if the mapping value type is itself an object) or just leaves the type from T (if the mapping value type is not an object, such as 1 above).

This works, but produces types that are hard to inspect:

type Z = PickPartial<UserType, { email: 1, address: { coordinates: { lat: 1, lng: 1 } } }>
/* type Z = Partial<Omit<UserType, "email" | "address">> & {
    email: string;
    address: PickPartial<{
        city: string;
        state: string;
        zip: string;
        coordinates: {
            lat: number;
            lng: number;
        };
    }, {
        coordinates: {
            lat: 1;
            lng: 1;
        };
    }>;
} */

Is that the type you want? It's hard to tell.


To remedy that, I will use a technique from How can I see the full expanded contract of a Typescript type? where we take the basic type, copy it into a new type argument via conditional type inference, and then do an identity mapping on it. That is, if you start with

type Foo = SomethingUgly

you can get nicer results with

type Foo = SomethingUgly extends infer O ? {[K in keyof O]: O[K]} : never;

So that gives us:

type PickPartial<T, M> = (
    Partial<Omit<T, keyof M>> & {
        [K in keyof T & keyof M]:
        M[K] extends object ? PickPartial<T[K], M[K]> : T[K]
    }
) extends infer O ? { [K in keyof O]: O[K] } : never;

Now if we try it we get:

type Z = PickPartial<UserType, { email: 1, address: { coordinates: { lat: 1, lng: 1 } } }>
/* type Z = {
    name?: {
        first: string;
        last: string;
    };
    email: string;
    address: {
        city?: string;
        state?: string;
        zip?: string;
        coordinates: {
            lat: number;
            lng: number;
        };
    };
} */

Which is the type you wanted.

Playground link to code

  • Related