Reputation: 1552
I'm working on an application where we have defined a type and need to deduce multiple interfaces out from that single interface.
Example:
Our main type looks something like this. It is not important if this type is defined as a mapped type or union type, so whatever solution is easiest would be the best.
type A = {
type: "A"
input: {
type: "inputA"
}
output: {
type: "outputB"
}
}
type B = {
type: "B"
input: {
type: "inputB"
}
output: {
type: "outputB"
}
}
type Definitions = A | B
From those definitions, we want to construct a type which encapsulates the function interfaces.
type Program<F, K> = F extends { type: K; input: infer I; output: infer O }
? (input: I) => O
: never
type Programs = { [K in Definitions["type"]]: Program<Definitions, K> }
// Correctly deduced type:
// type Programs = {
// A: (input: {
// type: "inputA";
// }) => {
// type: "outputA";
// };
// B: (input: {
// type: "inputB";
// }) => {
// type: "outputB";
// };
// }
We can then provide these functions, something like this:
const a = (input: { type: "inputA" }): { type: "outputA" } => {
return { type: "outputA" }
}
const b = (input: { type: "inputB" }): { type: "outputB" } => {
return { type: "outputB" }
}
const programs: Programs = {
A: a,
B: b,
}
So good so far. However, I can't seem to wrap my head around how to dynamically call these functions. It is important that the run1
and run2
functions are typesafe.
const run1 = (definition: Definitions) => programs[definition["type"]](definition.input)
// throws error
// Argument of type '{ type: "inputA"; } | { type: "inputB"; }' is not assignable to parameter of type 'never'.
// The intersection '{ type: "inputA"; } & { type: "inputB"; }' was reduced to 'never' because property 'type' has conflicting types in some constituents.
// Type '{ type: "inputA"; }' is not assignable to type 'never'.
// or
type Input<F> = F extends { input: infer I } ? I : never
type Output<F> = F extends { output: infer O } ? O : never
type Type<F> = F extends { type: infer T } ? T : never
const run2 = <F>(type: Type<F>, input: Input<F>): Output<F> => {
return programs[type](input)
}
// yields error
// Type 'Type<F>' cannot be used to index type 'Programs'.
Anything I'm missing?
Upvotes: 0
Views: 439
Reputation: 2539
I think the issue is that programs[definition["type"]]
returns the union of all the programs, so it's not callable (that's why the compiler error says not assignable to never).
You can get around this by using a switch statement:
const run1 = (definition: Definitions) => {
switch (definition.type) {
case "A":
return programs["A"](definition.input)
case "B":
return programs["B"](definition.input)
}
}
You can add an explicit return type to let Typescript warn you if you've missed a case in the switch statement as well:
const run1 = (definition: Definitions): Definitions["output"] => {
switch (definition.type) {
case "A":
return programs["A"](definition.input)
case "B":
return programs["B"](definition.input)
}
}
Upvotes: 2
Reputation: 33041
You are unable to call programs[definition["type"]]
because of contravariance.
Consider this example:
type A = {
type: "A"
input: {
type: "inputA"
}
output: {
type: "outputA"
}
}
type B = {
type: "B"
input: {
type: "inputB"
}
output: {
type: "outputB"
}
}
type Definitions = A | B
type Program<F, K> = F extends { type: K; input: infer I; output: infer O }
? (input: I) => O
: never
type Programs = { [K in Definitions["type"]]: Program<Definitions, K> }
const a = (input: { type: "inputA" }): { type: "outputA" } => {
return { type: "outputA" }
}
const b = (input: { type: "inputB" }): { type: "outputB" } => {
return { type: "outputB" }
}
const programs: Programs = {
A: a,
B: b,
}
const run1 = (definition: Definitions) => {
const { input } = definition
const fn = programs[definition["type"]] // expects union of A and B
if (input.type === 'inputA') {
fn()
}
}
Because of fn
arguments are in contravariant position, TS does intersection under the hood. Hence, {type:'A'} & {type:'B'} === never
That's why you are unable to call this function.
Here you can find a very good answer & explanation
You want union to intersection? Distributive conditional types and inference from conditional types can do that. (Don't think it's possible to do intersection-to-union though, sorry) Here's the evil magic:
Hence, if you want to make it work, I believe you should stick with @cdimitroulas approach.
AFAIK, TS does not play well with nested properties of union type. But I might be wrong
Upvotes: 2