2

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.

Thijs Koerselman
  • 21,680
  • 22
  • 74
  • 108
  • Please let me know if this https://stackoverflow.com/questions/69126879/typescript-deep-keyof-of-a-nested-object-with-related-type#answer-69129328 works for you. If not, you can check other links at the bottom of my article https://catchts.com/deep-pick . – captain-yossarian from Ukraine Nov 29 '21 at 17:42
  • 1
    @captain-yossarian Thanks! I changed the framing of my question but your link seems very relevant. I'll give that a try. – Thijs Koerselman Nov 29 '21 at 19:04
  • 1
    @captain-yossarian that code is absolutely brilliant. I have never seen anyone write types that clearly. It makes me want to learn more instead of feeling overwhelmed and demotivated. And I like your way of defining unnamed variables. I'm going to borrow that :) The code is almost what I need entirely. I only need to make it not iterate certain type properties like the FirebaseFirestore.Timestamp value. – Thijs Koerselman Nov 29 '21 at 21:07
  • 1
    @captain-yossarian Thanks to the quality of your code I was able to understand it and adapt it to my needs. Mainly preventing the Timestamp type from getting iterated over and not allowing arrays to be manipulated. You really saved my day! I was already spending hours on this and I don't think I would have figured this out on my own, so I am very grateful I will start reading your blog to see what other gems I can find :) – Thijs Koerselman Nov 29 '21 at 21:35
  • 1
    @captain-yossarian Is it ok if I post my final answer here? It will be 99% your code of course. Alternatively, I could just remove this question entirely... What would you suggest? – Thijs Koerselman Nov 29 '21 at 21:37
  • Thank you very much for your feedback. It is very motivating. Sure , you can write your answer. Please dont remove your question, I will show your feedback to my wife:D – captain-yossarian from Ukraine Nov 29 '21 at 22:13
  • 1
    LOL Let me know if it saved your marriage XD – Thijs Koerselman Dec 01 '21 at 09:18

0 Answers0