I have a set of interfaces in my Typescript code (autogenerated by OpenAPI Generator) and I would like to implement a generic method something like this:
interface Foo {}
function getFoo(id: string): Foo { /* ... */ }
interface Bar {}
function getBar(id: string): Bar { /* ... */ }
function get<T>(type: Type<T>, id: string): T {
switch(type) {
case typeof Foo:
return getFoo(id);
case typeof Bar:
return getBar(id);
default:
throw "Unknown type";
}
}
Is that possible?
If Foo and Bar were classes, I could have used
function get<T>(type: new() => T, id: string): T {
switch(typeof new type()){
// ...
}
}
, but this doesn't work with interfaces.
I could tag the interfaces and do
interface Foo { type: 'foo'; }
interface Bar { type: 'bar'; }
function get<T>(type: string, id: string): T {
switch(type) {
case 'foo':
return getFoo(id);
case 'bar':
return getBar(id);
default:
throw "Unknown type";
}
}
, but I don't see a way to preventing something like get<Bar>('foo', id)
.
CodePudding user response:
In order for this to possibly work, you'd need some mapping between genuine values, like the strings "Foo"
and "Bar"
, to the corresponding interface types, like Foo
and Bar
. You'd pass a value into get()
, along with an id of some sort, and then get a value of the corresponding interface type.
So, what values should we use? Strings are a good choice since they're easy to come by, and there's a very straightforward way to represent mappings between string literal types and other types: object types. They are already mappings between keys (of string literal types) and values (or arbitrary types).
For example:
interface TypeMapper {
"Foo": Foo;
"Bar": Bar;
}
Which can equivalently be written
interface TypeMapper {
Foo: Foo;
Bar: Bar;
}
Armed with a type like that, then get()
should have the generic call signature
declare function get<K extends keyof TypeMapper>(
type: K, id: string
): TypeMapper[K];
whereby the type
input is of type K
constrained to keyof TypeMapper
, and the output is of the indexed access type TypeMapper[K]
.
Let's just imagine we already have that implemented, and make sure you could call it as desired:
const foo = get("Foo", "abc");
// const foo: Foo
foo.a; // it's a Foo
const bar = get("Bar", "def");
//const bar: Bar
Looks good.
Now for an implementation of get()
. You could write it similar to in your question:
function get<K extends keyof TypeMapper>(type: K, id: string): TypeMapper[K] {
switch (type) {
case 'Foo':
return getFoo(id); // error! Type 'Foo' is not assignable to type 'TypeMapper[K]'.
case 'Bar':
return getBar(id); // error! Type 'Bar' is not assignable to type 'TypeMapper[K]'.
default:
throw "Unknown type";
}
}
This works at runtime, and the typings are correct, but unfortunately the compiler can't verify that. When you check type
with case 'Foo'
, it can narrow down type
from type K
to "Foo"
, but it doesn't know how to narrow the type parameter K
itself, and so it doesn't see that a value of type Foo
is assignable to TypeMapper[K]
. This is currently a limitation of TypeScript, and there are various open feature requests asking for some improvement. For example, microsoft/TypeScript#33014. Until and unless such a feature is implemented, you will need to work around the limitation.
The easiest approach is to just suppress the errors with type assertions:
function get<K extends keyof TypeMapper>(type: K, id: string): TypeMapper[K] {
switch (type) {
case 'Foo':
return getFoo(id) as TypeMapper[K];
case 'Bar':
return getBar(id) as TypeMapper[K];
default:
throw "Unknown type";
}
}
That works, but now you have the responsibility of implementing it properly, since the compiler can't. If you had swapped case 'Foo'
with case 'Bar'
, the compiler wouldn't have noticed:
function getTwoBad<K extends keyof TypeMapper>(type: K, id: string): TypeMapper[K] {
switch (type) {
case 'Bar': //