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.