Let's say that I have the following entity model:
interface Entity {
id: string;
}
interface Member extends Entity {
group: Group | string;
}
interface Group extends Entity {
members: (Member | string)[];
}
I will be separately retrieving lists of both Group and Member entities. At retrieval, the entities will contain string references to other entities. E.g., a Member entity will have a group property containing the ID of the group that the member belongs to.
Now, after retrieval, I want to hydrate/inflate the entities by replacing the string references with the entities that they refer to. E.g., the following call should replace the group references in Member entities with the actual Group objects:
link(members, groups, 'group');
Now, I want to restrict the third parameter in that call ('group') to only allow property names that can potentially refer to a Group entity, so I write the following type definition:
type Ref<T, S> = { [K in keyof T]: S extends T[K] ? K : never }[keyof T];
This works correctly, e.g. type X = Ref<Member, Group> will evaluate to group.
The method implementation could then look like this:
function link<T extends Entity, S extends Entity>(target: T[], source: S[], ref: Ref<T, S>) {
const lookup = new Map(source.map<[string, S]>(entity => [entity.id, entity]));
target.forEach(entity => {
const value = entity[ref];
entity[ref] = typeof value === 'string' ? lookup.get(value) || value : value; // error
});
}
Unfortunately, this throws compiler errors:
Type 'S | T[{ [K in keyof T]: S extends T[K] ? K : never; }[keyof T]]' is not assignable to type 'T[{ [K in keyof T]: S extends T[K] ? K : never; }[keyof T]]'.
Type 'S' is not assignable to type 'T[{ [K in keyof T]: S extends T[K] ? K : never; }[keyof T]]'.
Type 'Entity' is not assignable to type 'T[{ [K in keyof T]: S extends T[K] ? K : never; }[keyof T]]'.(2322)
I believe that, with TypeScript's rich type system, it should be perfectly possible to come up with an elegant solution, but I can't seem to get it right. What am I missing here?
Here's a link to the TypeScript Playground for those who want to take a stab at it.
Note that I'm looking for an elegant ideomatic TypeScript solution, and not for a hack to coerce the compiler. If I wanted that, I might as well write plain JavaScript.