2

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?

Lance
  • 75,200
  • 93
  • 289
  • 503
  • Not at my PC at the moment, so hard to check anything, but here's my 2c on the second API idea. First param is `Input`, second is `Pattern`, which extends `DeepPartial`. The type is then `DeepPartial & Pattern`. – Darryl Noakes Dec 21 '22 at 02:41
  • Monaco (the editor used in VS Code and the TypeScript playground) is nigh unusable on mobile. – Darryl Noakes Dec 21 '22 at 02:44
  • Does [this approach](https://tsplay.dev/Nd2OkW) meet your needs? I've used a different mapping object than either of your versions but hopefully it's intuitive. If it works for you I'll write up an answer explaining; if not, what am I missing? (Please mention @jcalz in your reply to notify me) – jcalz Dec 21 '22 at 03:12
  • @jcalz yeah that is looking good! I don't fully understand it yet, perhaps you could write up an answer and explain how it works more. – Lance Dec 21 '22 at 03:25
  • I will write up a full answer when I get a chance. – jcalz Dec 21 '22 at 03:26

1 Answers1

2

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

jcalz
  • 264,269
  • 27
  • 359
  • 360