1

I want to enhance return type of a function based on a type parameter.

Let's say I have entity with property ref1 of type Reference<User>, and based on the type parameter I want to enhance it to LoadedReference<User> if ref1 (or ref1.something, or ref1.something.something...) is part of the type parameter value.

The demonstration of this problem follows:

// T1 is what I want for all of those
type T1 = LoadedIfInKeyHint<Book, 'ref1', 'ref1.books'>
// T2 is the problem, as it resolves to a union
type T2 = LoadedIfInKeyHint<Book, 'ref1', 'ref1' | 'ref1.books'>
// T3 demonstrates that the problem is just with having both `ref1` and `ref1.books` in the input type
type T3 = LoadedIfInKeyHint<Book, 'ref1', 'ref1.books' | 'title' | 'id'>

declare const t1: T1;
declare const t2: T2;
declare const t3: T3;
console.log(t1.$.books.$[0].title) // works as expected
console.log(t2.$.books.$[0].title) // does not work as `books` resolves to union
console.log(t3.$.books.$[0].title) // works as expected

LoadedIfInKeyHint type is used to resolve the type of object properties. It is used recursively via the Loaded type. Their definition follows:

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]>
  : never;

export type Loaded<T, P = unknown> = unknown extends P ? T : T & Simplify<{
  [K in keyof RelationsIn<T>]: LoadedIfInKeyHint<T, K, P>;
}>

(the Simplify type just removes properties that would resolve to never)

Playground link that should not produce any errors.

(Follow up to Mapping of strict path notation array type question that describes the end goal.)

Martin Adámek
  • 16,771
  • 5
  • 45
  • 64

1 Answers1

3

I think that here:

type T2 = LoadedIfInKeyHint<Book, 'ref1', 'ref1' | 'ref1.books'>

The problem is that it resolves to something like:

type T2 =
  LoadedReference<User, User & Simplify<{
    friends: LoadedCollection<User, User>;
    books: never;
  }>>
  | LoadedReference<User, User & Simplify<{
    friends: never
    books: LoadedCollection<User, User>;
  }>>

That could be fixed by converting the union to intersection:

type UnionToIntersection<U> = 
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

type FixedLoadedIfInKeyHint<T, K extends keyof T, H> = UnionToIntersection<LoadedIfInKeyHint<T, K, H>>;

Oh, and a playground link.

Note that I didn't look at the possibility of avoiding the union in the first place, I'm not sure whether that's possible or not.


A few things I noticed, not sure if I'm right about them or what's their context.

This may be just a strange example in the playground – I noticed you're using the following line:

// T3 demonstrates that the problem is just with having both `ref1` and `ref1.books` in the input type
type T3 = LoadedIfInKeyHint<Book, 'ref1', 'ref1.books' | 'title' | 'id'>

But the title and id part will have no effect. Looking at your type definition of LoadedIfInKeyHint:

type LoadedIfInKeyHint<T, K extends keyof T, H> = H extends `${infer A}.${infer B}`
    ? (A extends K ? MarkLoaded<T, T[A], B> : never) // 1st branch
    : (K extends H ? MarkLoaded<T, T[K]> : never);   // 2nd branch

We can see that those will go to the 2nd branch which only looks if ref1 exists in the listed property names (and if so, it marks the property loaded).

Also, looking at the current MarkLoaded<T, P, H = unknown> I wonder whether T is of any use (it's returned when P is neither reference nor a collection, which seems a bit strange to me). Wouldn't dropping paraeter T and using never do the same job?

type MarkLoaded<P, H = unknown> = P extends Reference<infer U>
  ? LoadedReference<U, Loaded<U, H>>
  : P extends Collection<infer U> ? LoadedCollection<U, Loaded<U, H>> : never; // here "never"
Jan Jakeš
  • 2,449
  • 1
  • 13
  • 10