1

This is a follow-up to TS strict paths with dot notation in array question, where I was trying to make the strict path notation work with arrays in MikroORM. Now I am trying to map this type to a Loaded return type that marks the populated Collection and Reference properties to their Loaded* variants recursively.

The problematic bit will be most probably in LoadedIfInKeyHint subtype that is should resolve to either the original property type (if not populated/found in the P populate hint), or to LoadedReference/LoadedCollection (if it was populated based on P).

It works fine if there is one item in the populate array, even for nested hints, but once there are more arrays, it fails to recurse. The question is basically only about r022 and r023 variables where the return type is wrong, while r021 is correct. All expected errors are marked with @ts-expect-error comments, the playground link now produces two errors that are not expected.

Full playground link that should ideally produce no errors.

Demonstration of the problem:

declare const user: User;
declare const book: Book;
declare function find<T, P extends string>(obj: T, options: FindOptions<T, P>): Loaded<T, P>;

const r010 = find(book, { populate: ['author'] })
const r011 = find(book, { populate: ['ref1'] })
const r021 = find(book, { populate: ['ref1.books'] })
console.log(r021.ref1.$.books.$[0].author);
// @ts-expect-error
console.log(r021.ref1.$.books.$[0].ref1.$);

// this fails as the type of `ref1` is resolved to `LoadedReference<User, User>` instead of 
// `LoadedReference<User, User & {
//   books: LoadedCollection<Book, Book>;
//   friends: Collection<User>;
// }>;`
// as in `r021` that populates just `ref1.books` and not `ref1`
// (the `title` here should be irrelevant, as `r023` reproduces the same problem without it)
const r022 = find(book, { populate: ['ref1', 'ref1.books', 'title'] })
console.log(r022.ref1.$.books.$[0].author);

// @ts-expect-error
console.log(r022.ref1.$.books.$[0].ref1.$);
const r023 = find(book, { populate: ['ref1.books', 'ref1'] })
console.log(r023.ref1.$.books.$[0].author);
// @ts-expect-error
console.log(r023.ref1.$.books.$[0].ref1.$);
const r11 = find(user, { populate: ["friends", 'books'] })
const r21 = find(user, { populate: ["friends.books.author", 'books'] })
const r31 = find(user, { populate: ["books.author"] })
const r2 = find(user, { populate: ["friend.books", 'age'] })
const r3 = find(user, { populate: ["friend.books.ref1.age", 'friend.books.ref1.name'] })
const r4 = find(user, { populate: ["friend.books.ref1"] })
const r5 = find(user, { populate: ["friend.name", "books.author.name"] })
const r6 = find(user, { populate: ["friend.name", "books.author.friend.age", "age", 'books.title'] })

Code for AutoPath and Loaded types:

class Collection<T> { items?: T[] }
class Reference<T> { item?: T }

type Book = {
  id: string,
  title: string,
  author: User,
  ref1: Reference<User>,
}

type User = {
  id: string,
  name: string,
  age: number,
  friend: User,
  friends: Collection<User>,
  books: Collection<Book>,
}

type ExtractType<T> = T extends Collection<infer U> ? U : (T extends Reference<infer U> ? U : T)
type StringKeys<T> = T extends Collection<any> 
  ? `${Exclude<keyof ExtractType<T>, symbol>}` 
  : T extends Reference<any>
    ? `${Exclude<keyof ExtractType<T>, symbol>}` 
    : T extends object
      ? `${Exclude<keyof ExtractType<T>, symbol>}` 
      : never
type GetStringKey<T, K extends StringKeys<T>> = K extends keyof T ? ExtractType<T[K]> : never

type AutoPath<O, P extends string> =
  P extends any ?
  (P & `${string}.` extends never ? P : P & `${string}.`) extends infer Q
    ? Q extends `${infer A}.${infer B}`
      ? A extends StringKeys<O>
        ? `${A}.${AutoPath<GetStringKey<O, A>, B>}`
        : never
      : Q extends StringKeys<O>
        ? (GetStringKey<O, Q> extends unknown ? Exclude<P, `${string}.`> : never) | (StringKeys<GetStringKey<O, Q>> extends never ? never : `${Q}.`)
        : StringKeys<O>
    : never
  : never

export interface FindOptions<T, P extends string> {
  populate?: AutoPath<T, P>[] | boolean;
}

export type Populate<T, P extends string = string> = AutoPath<T, P>[] | boolean;

export interface LoadedReference<T, P = never> extends Reference<T> {
  $: T & P;
  get(): T & P;
}

export interface LoadedCollection<T, P = never> extends Collection<T> {
  $: readonly (T & P)[];
  get(): readonly (T & P)[];
}

export type ExpandProperty<T> = T extends Reference<infer U>
  ? NonNullable<U>
  : T extends Collection<infer U>
    ? NonNullable<U>
    : T extends (infer U)[]
      ? NonNullable<U>
      : NonNullable<T>;

type MarkLoaded<T, P, H = unknown> = P extends Reference<infer U>
  ? LoadedReference<U, Loaded<U, H>>
  : P extends Collection<infer U>
    ? LoadedCollection<U, Loaded<U, H>>
    : T;

type LoadedIfInKeyHint<T, K extends keyof T, H> = [H] extends [`${infer A}.${infer B}`] 
    ? A extends K
      ? MarkLoaded<T, T[A], B> 
      : never
    : [K] extends [H] 
    ? MarkLoaded<T, T[K]> 
  : T[K];

// https://medium.com/dailyjs/typescript-create-a-condition-based-subset-types-9d902cea5b8c
type SubType<T, C> = Pick<T, { [K in keyof T]: T[K] extends C ? K : never }[keyof T]>;

type RelationsIn<T> = SubType<T, Collection<any> | Reference<any> | undefined>;

export type Loaded<T, P = unknown> = unknown extends P ? T : T & {
  [K in keyof RelationsIn<T>]: LoadedIfInKeyHint<T, K, P>;
}
Martin Adámek
  • 16,771
  • 5
  • 45
  • 64
  • Could you please mark exact line where do you have an error but it should be ok. I see that all tests from r6 to r11 are ok , even if they contain more items inside array – captain-yossarian from Ukraine Jul 20 '21 at 12:36
  • If you open the playground link you will see, all expected errors are marked with `@ts-expect-error` comments. It is also commented here in the question, specifically the `r022` and `r023` variables are both typed wrongly. – Martin Adámek Jul 20 '21 at 12:37
  • So all `@ts-expect-error` should be ok, correct? – captain-yossarian from Ukraine Jul 20 '21 at 12:38
  • Yes, the question is basically only `r022` and `r023` which both fail to recurse as `r021` does. The rest is to ensure it otherwise works as expected. – Martin Adámek Jul 20 '21 at 12:40
  • Oof, I don't know when or if I'll have time to dissect this fairly complicated code. If you can produce something more minimal with the same problem, I'd be more likely to make progress. That probably goes for other people who want to help, as well. – jcalz Jul 21 '21 at 16:16
  • Understood. I already made some progress and narrowed the problem a bit, here is a follow up question that should be hopefully more narrow: https://stackoverflow.com/questions/68484304/enhancing-object-properties-based-on-type-parameter – Martin Adámek Jul 22 '21 at 11:48

0 Answers0