Mateusz
Mateusz

Reputation: 69

How can I infer type from interface with generically typed props

I have setup like this

interface IServiceRegistry<T> {
  [id: string]: T;
}


interface ExampleService {
    a: number;
}

interface AnotherExampleService {
    b: string;
}

interface IRegistry {
  exampleService: IServiceRegistry<ExampleService>;
  anotherExampleService: IServiceRegistry<AnotherExampleService>;
}

type GetService<M> = M extends IServiceRegistry<infer X> ? X : never;

function getService<K extends keyof IRegistry>(
  serviceType: K,
  serviceName: string,
): GetService<IRegistry[K]> {
  return registry[serviceType][serviceName];
}

const registry = {} as IRegistry;

so then I can the following and have all typing support

const s1 = getService('exampleService', 'someName');
// const s1: ExampleService

const s2 = getService('anotherExampleService', 'someName');
// const s2: AnotherExampleService

Unfortunately getService function return type GetService<IRegistry[K]> doesn't work in this case.

Type 'ExampleService | AnotherExampleService' is not assignable to type 'GetService<IRegistry[K]>'. Type 'ExampleService' is not assignable to type 'GetService<IRegistry[K]>'.

How to get proper typing work for getService function?

Upvotes: 1

Views: 86

Answers (1)

jcalz
jcalz

Reputation: 328282

The problem with GetService<IRegistry[K]> is that, inside the body of getService(), it's a conditional type that depends on an unspecified generic type parameter K. The TypeScript type checker really doesn't know how to do much with such types; it tends to defer evaluation of them, leaving them as essentially opaque. The compiler really doesn't know what values might be assignable to GetService<IRegistry[K]> and it doesn't try very hard to figure it out. This is currently a known limitation of TypeScript, and there have been suggestions to do something better, like microsoft/TypeScript#33912. For now, though, if you want to use types like this, you'll pretty much need to loosen type safety via something like type assertions:

function getService<K extends keyof IRegistry>(
    serviceType: K,
    serviceName: string,
): GetService<IRegistry[K]> {
    return registry[serviceType][serviceName] as GetService<IRegistry[K]>; // 🤷‍♂️
}

But you don't need to write GetService<M> as a conditional type. All you're doing is looking up the property type for the index signature of M. So instead of

type GetService<M> = M extends IServiceRegistry<infer X> ? X : never;

you could write

type GetService<M extends IServiceRegistry<any>> = M[string];

Or we can dispense with it entirely and just use M[string] directly.

So that gives us:

function getService<K extends keyof IRegistry>(
    serviceType: K,
    serviceName: string,
): IRegistry[K][string] {
    return registry[serviceType][serviceName]; // error!
}

which, unfortunately, still doesn't work. I'm not 100% sure why this happens; analogous constructs tend to work. After all, I'm indexing into a value of type IRegistry with a key of type K to get a value of type IRegistry[K], and then indexing into that with a key of type string to get a value of type IRegistry[K][string]. But it's not working. I filed microsoft/TypeScript#51127 to find out why, but for now this is some sort of design limitation in TypeScript.


Let's work around it. The easiest way I can see to do that is to change serviceType from string to string & keyof IRegistry[K]. This should be equivalent to string, but now the compiler should understand that we're indexing into IRegistry[K] with keyof IRegistry[K]:

function getService<K extends keyof IRegistry>(
    serviceType: K,
    serviceName: string & keyof IRegistry[K],
): IRegistry[K][typeof serviceName] {
    return registry[serviceType][serviceName]; // okay
}

Now that compiles without error, hooray! And you get the behavior you're looking for:

const s = getService('exampleService', 'someName');
// const s: ExampleService

Playground link to code

Upvotes: 3

Related Questions