typescripttypesnestedtypescript-typingskeyof

Typescript type safe string with dot notation for query nested object


I am working with Typescript and firebase and I have a small abstraction layer with this function to search for a unique document base on its field name and its value.

  where<K extends keyof (T & DocumentEntity)>(fieldName: K, operator: WhereFilterOp, value: unknown): Query<T> {
    this.addCriterion(new WhereCriterion(fieldName as string, operator, value));
    return this;
  }

This works well when I want to query with a field at the base of the document, for example:

Model:

order: Order = {
  orderId: baseId
  item: { ... }
  price: { ... }
  restaurant: {
    restaurantId: nestedId
    name: chezGaston
  }
}

Query:

    const order = await this.documentPersistence.findUnique(
      new Query<order>().where('orderId', '==', incomingOrderId)
    );

But now I want to query base on the id of a nested object.

const order = await this.documentPersistence.findUnique(
      new Query<order>()
        .where('restaurant.restaurantId', '==', integration),
    );

And this gives me a static error TS2345: Argument of type '"restaurant.restaurantId"' is not assignable to parameter of type 'keyof Order'.

How can I fix my function so it accepts Nested object as keyof my object?

I don't want to use // @ts-ignore


Solution

  • You can do this as of TypeScript 4.1.

    Click the playground example to see it in action:

    TypeScript Playground

    Original Twitter Post

    Here's the relevant code:

    type PathImpl<T, K extends keyof T> =
      K extends string
      ? T[K] extends Record<string, any>
        ? T[K] extends ArrayLike<any>
          ? K | `${K}.${PathImpl<T[K], Exclude<keyof T[K], keyof any[]>>}`
          : K | `${K}.${PathImpl<T[K], keyof T[K]>}`
        : K
      : never;
    
    type Path<T> = PathImpl<T, keyof T> | keyof T;
    
    type PathValue<T, P extends Path<T>> =
      P extends `${infer K}.${infer Rest}`
      ? K extends keyof T
        ? Rest extends Path<T[K]>
          ? PathValue<T[K], Rest>
          : never
        : never
      : P extends keyof T
        ? T[P]
        : never;
    
    declare function get<T, P extends Path<T>>(obj: T, path: P): PathValue<T, P>;
    
    const object = {
      firstName: "Diego",
      lastName: "Haz",
      age: 30,
      projects: [
        { name: "Reakit", contributors: 68 },
        { name: "Constate", contributors: 12 },
      ]
    } as const;
    
    get(object, "firstName"); // works
    get(object, "projects.0"); // works
    get(object, "projects.0.name"); // works
    
    get(object, "role"); // type error