4

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

3
  • I looked at this question for a potential answer, however I was unable to get it to work: stackoverflow.com/questions/54714232/… Commented May 1, 2019 at 8:56
  • How is makeModel supposed to be called? Commented May 1, 2019 at 12:52
  • makeModel is really just an artifact of my real code, you can ignore its naming. In practice, I have external data streaming in, and I transform that data based on the __type property. I fetch data from an endpoint, convert it to a schema (using io-ts), and then convert it to a "model" (it gains certain methods depending on __type). Commented May 1, 2019 at 16:32

2 Answers 2

3

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!

Sign up to request clarification or add additional context in comments.

1 Comment

Really appreciate the insight, and links to the TypeScript documentation and GitHub issues for your answers.
1

Another way to get around this error is to define generic version of makeModel which can work on arbitrary registry, then define specific version for one instance of registry:

export const makeAnyModel = <Registry extends { [N in string]: Model<N> }>
(registry: Registry, name: keyof Registry) => (
  options: Parameters<Registry[keyof Registry]["getName"]>[0],
) => {
    const model = registry[name];
    return model.getName(options);
};

export const makeModel = <N extends keyof ModelRegistry>(name: N) => 
    makeAnyModel(modelRegistry, name);

There are no errors in makeAnyModel because N is assumed to be string when typechecking the implementation - inferred type for getName is

Model<string>.getName: (options: { __type: string; }) => string

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.