Final conclusion: typescript sucks, Ocaml is better
EDIT
After adding generic types and avoiding function union, here's what I have. Same exact problem, though.
const fn1: ((obj: { A: string }) => string) = (input) => { return input.A }
const fn2: ((obj: { B: number }) => number) = (input) => { return input.B }
const fns = { fn1, fn2, }
type allowedFns = keyof typeof fns // 'fn1' | 'fn2'
const caller = <fnName extends allowedFns>(fn: (typeof fns)[fnName], input: Parameters<(typeof fns)[fnName]>[0]) => {
fn(input)
}
Original post
Here is a very basic example I came up with. I want caller
to take in fn
and input
and call fn(input)
. fn
is only allowed to be of a type in allowedFns
.
type allowedFns = ((obj: { A: string }) => any) | ((obj: { B: number }) => any)
const caller = (fn: allowedFns, input: { A: string } | { B: number }) => {
return fn(input) /* ERROR: Argument of type '{ A: string; } | { B: number; }'
is not assignable to parameter of type '{ A: string; } & { B: number; }' */
}
I'm getting an error (see comment). fnType is being incorrectly typed! The following is how it's currently being typed:
(parameter) fn: (obj: { A: string; } & { B: number; }) => any
But it should really be typed as follows:
(parameter) fn: (obj: { A: string; } | { B: number; }) => any
Why does the |
of functions combine their inputs like an &
??? And is there a fix?
CodePudding user response:
In a union of functions it is only safe to invoke it with an intesection of parameters.
You will need an generic to associate the right function to the right input.
It could look something like that
function caller<T extends {}>(fn: (obj: T) => any, input: T) {
fn(input)
}
CodePudding user response:
The answer to the question "why":
It might be confusing that a union of types appears to have the intersection of those types’ properties. This is not an accident - the name union comes from type theory. The union number | string is composed by taking the union of the values from each type. Notice that given two sets with corresponding facts about each set, only the intersection of those facts applies to the union of the sets themselves. For example, if we had a room of tall people wearing hats, and another room of Spanish speakers wearing hats, after combining those rooms, the only thing we know about every person is that they must be wearing a hat.
Typescript: Working with union types
The solution is easy, if you have fields with the same name:
Since your fields have different names, solution looks like this:
type Arguments = { A: number } | { B: string }
function caller<Type extends Arguments>(
fn: (obj: Type ) => any,
argument: Type
) {
return fn(argument);
}
const f1 = (obj: { A: number }) => console.log(obj.A 11);
const f2 = (obj: { B: string }) => console.log("value: " obj.B.toUpperCase());
caller(f1, { A: 1 });
caller(f2, { B: "maybe?" })
And even:
const ultimateArgument = { A: 123, B: "ufo"}
caller(f1, ultimateArgument); // 124
caller(f2, ultimateArgument); // value: UFO