Reputation: 69
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
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
Upvotes: 3