I am working with a document store (Firestore) and I'm trying to write a typed document update
function which takes a field path. This field path should allow you to point to any of the fields in the type but not inside arrays.
I found a similar question here, but what I'm going for is a little different.
Based on that answer and lots of trial and error I arrived at this:
export type DeepKeyOf<T> = (
T extends object
? {
[K in Extract<keyof T, string>]: T[K] extends Array<unknown>
? `${K}`
: `${K}` | `${K}${DotPrefix<DeepKeyOf<T[K]>>}`;
}[Extract<keyof T, string>]
: ""
) extends infer D
? Extract<D, string>
: never;
type DotPrefix<T extends string> = T extends "" ? "" : `.${T}`;
export type DeepKeyMap<T> = { [P in DeepKeyOf<T>]?: any };
type ExampleDocument = {
a: { b: { c: string; d?: number; arr: number[] } };
arr: string[];
};
The type of DeepKeyOf<ExampleDocument>
is giving me this:
"arr" | "a" | "a.b" | "a.b.c" | "a.b.d" | "a.b.arr"
If a user is allowed to set a.b.c
it is also allowed to set any of its parents, which includes a.b
and a
.
Note that I'm really only dealing with simple POJO objects here, and a document type would never contain functions or symbols. And values can be (simple objects) containing string, boolean, number, null, or arrays thereof.
Optional fields should be treated as other non-optional fields.
For mutating arrays Firestore has dedicated functions, so I would prefer to keep it out of the type. Updating the array as a whole should be allowed. So in that sense, the generated type is usable for me to define all possible field paths.
But instead of only being able to type the keys I would also like to type the value for each of them. So what I'm really after is this:
type NestedFieldPaths = {
"a.b"?: { c: string; d?: number; arr: number[] };
"a.b.c"?: string;
"a.b.d"?: number;
"a.b.arr"?: number[];
};
Note that I'm leaving out the root keys here, because those could already be defined by Partial<ExampleDocument>
, but of course if they end up directly in the above type that's fine too.
Then the update() function argument would be NestedFieldPaths | Partial<ExampleDocument>
. I think this comes closest to what Firestore allows you to do with an update call.