Goal: make the return type of a function based upon the input type using a mapped type to lookup the return type.
Problem: I get an error because of the conflicts in intersection of discriminated union types, which I conceptually understand but am at a loss on how to structure my types to achieve the my goal.
Type 'Integration' is not assignable to type 'IntegrationTypeData[T]'.
Type 'IntegrationA' is not assignable to type 'IntegrationTypeData[T]'.
Type 'IntegrationA' is not assignable to type 'never'.
The intersection 'IntegrationA & IntegrationB' was reduced to 'never' because property 'name' has conflicting types in some constituents.
My type look like the following:
enum Integrations {
A = 'A',
B = 'B',
}
type IntegrationMap<M extends { [key: string]: any }> = {
[Key in keyof M]: M[Key]
};
type IntegrationA = {
name: Integrations.A,
propertyA: string;
}
type IntegrationB = {
name: Integrations.B,
propertyB: number;
}
type IntegrationData = {
[Integrations.A]: IntegrationA;
[Integrations.B]: IntegrationB;
};
// this resolves to type Integration = IntegrationA | IntegrationB
type Integration = IntegrationMap<IntegrationData>[keyof IntegrationMap<IntegrationData>];
// the function I want to make have a dynamic/mapped return type
const getIntegration = <T extends Integrations>(name: T): IntegrationData[T] => {
// the type of the integrations variable is Integration[]
const integration = integrations.find((i: Integration) => i.name === name);
return integration;
};
// desired usage
// typeof intB = IntegrationB
const intB = getIntegration(Integrations.B);
Can someone help me understand how to resolve this error and how to properly type the data so that I get a dynamic and type safe return type of this function?
The elements of the integrations
array are of the union type (IntegrationA | IntegrationB)
.
If you look at the default call signature for the find()
method of arrays, you'll see that it returns a value of either the element type (IntegrationA | IntegrationB
in this case) or undefined
. So you are returning a value of type IntegrationA | IntegrationB | undefined
. But since your function's return type is IntegrationData[T]
where T
is the type of the name
argument passed in. The compiler cannot be sure that the former is assignable to the latter, so it complains.
The particular error message you get talks about the intersection type IntegrationA & IntegrationB
being reduced to the never
type because the only way to be sure that you're returning an Integration[T]
independently of T
is if your value were both an IntegrationA
and an IntegrationB
, which is impossible. It's probably best not to get too caught up in the error message.
Suffice it to say that the problem here is that find()
's return type is too wide for your purposes.
Luckily for you there is another call signature for find()
which has the potential to return a narrower type than the array element type (but still possibly undefined
). If the callback you pass in is a user-defined type guard function with a return type predicate of the form x is Y
, then will return the narrowed type Y | undefined
.
It would be nice if the compiler could understand that i => i.name === name
acts as a type guard function with call signature (i: Integration) => i is IntegrationData[T]
. Unfortunately this does not happen; TypeScript does not infer type guard function call signatures from implementations. There is an open request at microsoft/TypeScript#38390 to support this in some cases, but for now it's not part of the language. (And even if it were, such inference in this situation might be beyond its abilities.)
If you want to use i => i.name === name
as a type guard function you need to annotate it as such:
const getIntegration = <T extends Integrations>(name: T): IntegrationData[T] => {
const integration = integrations.find(
(i: Integration): i is IntegrationData[T] => i.name === name
// annotated ---> ^^^^^^^^^^^^^^^^^^^^^^^^^
);
return integration!;
};
The annotation gets you most of the way there. It still doesn't eliminate undefined
, though. For all the compiler knows, integrations
doesn't actually have an element of the type IntegrationData[T]
in it. So integration
might be undefined
. You can handle this explicitly, by writing if (!integration) throw new Error("OH NO I WAS WRONG")
before return
. Or, if you're sure it won't actually be undefined
, you can tell the compiler that by using a non-null assertion operator (!). Writing return integration!
, you're claiming that integration
will not be undefined
or null
, and thus the value of type IntegrationData[T] | undefined
can be treated like an IntegrationData[T]
.
And now it compiles with no error.