Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript unexpected intersection

Tags:

typescript

I have a registry of "models" and when I select a model from the registry, and call a method on it, TypeScript expects an intersection of the parameters across all registered models.

For brevity, I've reproduced this bug with a dummy method "getName".

export class Model<N extends string> {
  public name: N;

  constructor(name: N) {
    this.name = name;
  }

  public getName = (options: { __type: N }) => options.__type;
}

export const Book = new Model("Book");
export const User = new Model("User");

export const modelRegistry = { Book, User };

export type ModelRegistry = typeof modelRegistry;

export const makeModel = <N extends keyof ModelRegistry>(name: N) => (
  options: Parameters<ModelRegistry[N]["getName"]>[0],
) => {
  const model = modelRegistry[name];
  return model.getName(options); // <-- bug: TS expects this to be { __type: User } & { __type: Book }
};

enter image description here

Playground Link

like image 657
Devin Avatar asked Nov 15 '25 22:11

Devin


1 Answers

The problem here is that the compiler doesn't know how to interpret the generic ModelRegistry[N]["getName"] as correlated to the type N when you try to call it, and instead widens N to the full union type keyof ModelRegistry. Therefore it sees model.getName as a union of function types of different parameter types. Before TypeScript 3.3 such a union was just not callable at all (see microsoft/TypeScript#7294). In TypeScript 3.3 support was added to allow such functions to be called with an intersection of the parameters for each function in the union. That's better than "not callable", but it still leaves a lot to be desired, especially in the case you have here where there are correlated union types involved (see microsoft/TypeScript#30581).

In cases like these the easiest way to address it is to accept that you are smarter than the compiler and assert the types you expect to see. To demonstrate I'll just use as any to silence the compiler:

export const makeModel = <N extends keyof ModelRegistry>(name: N) => (
  options: Parameters<ModelRegistry[N]["getName"]>[0],
): ReturnType<ModelRegistry[N]["getName"]> => {
  const model = modelRegistry[name];
  return model.getName(options as any) as any; // I'm smarter than the compiler 🤓
};

This should work for you... you could tighten the any assertions up to more applicable types, if you desire. Note that I've manually annotated the return type of the curried makeModel call to be ReturnType<ModelRegistry[N]["getName"], since the compiler is unlikely to be able to figure that out.

Anyway, hope that helps. Good luck!

like image 99
jcalz Avatar answered Nov 18 '25 11:11

jcalz