0

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?

ghybs
  • 47,565
  • 6
  • 74
  • 99
Lucas Mumbo
  • 162
  • 8
  • `fn` could be any of the two. So for the input to work, it has to satisfy both criteries.. which actually makes a lot of sense if you think about it. Otherwise you would be able to pass the `A` version of the function and the `B` input type. – super Aug 17 '22 at 19:29

2 Answers2

4

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)
}
Matthieu Riegler
  • 31,918
  • 20
  • 95
  • 134
  • I'm trying with a generic but I'm running into the same problem, since to do it efficiently, at some point you need to use a union of functions... The only alternative I can think of is using a union of strings to look up the functions, which is not the most intelligent way of doing things... – Lucas Mumbo Aug 17 '22 at 19:43
  • Thanks for your comment! I'm having trouble with the details of the generic type. It seems no matter what, the same & problem arises. – Lucas Mumbo Aug 17 '22 at 19:48
  • @AndrewP. why would my exemple be a match. ? – Matthieu Riegler Aug 17 '22 at 19:52
  • If you actually try to implement it instead of stopping at the vague idea that generics will help, I would bet you'll run into the same problem. – Lucas Mumbo Aug 17 '22 at 19:53
  • 1
    @AndrewP. Usually union of functions is not what you are looking for. See [my article](https://catchts.com/react-props) and my [dev.to](https://dev.to/captainyossarian/how-to-type-react-props-as-a-pro-2df2) article. Apart from that you might be interested in [this](https://stackoverflow.com/a/50375286) answer – captain-yossarian from Ukraine Aug 22 '22 at 06:01
0

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

Additionally, you can deduct Arguments from your function signatures

type AllowedFunctions = ((obj: { A: number }) => void)
                      | ((obj: { B: string }) => void)
                      | ((obj: { C: { D : string } }) => void);

like so:

type Arguments = Parameters<AllowedFunctions>[0]

or, if you want to be more cautious:

type Argument<Function> = Function extends ((obj: infer P) => any) ? P : never
type Arguments = Argument<AllowedFunctions>

Here's code fragment.

Pavel K
  • 13
  • 4
  • I don't have fields with the same name :P – Lucas Mumbo Aug 17 '22 at 20:22
  • 1
    That's not a solution. That a work-around without any benefit. I don't think OP is asking how to make the compiler happy. He wants proper type deduction on the passed in parameters. – super Aug 17 '22 at 20:53
  • You can pass correct combination of function and value and it compiles, or incorrect and then it doesn't compile. The only "cheat" that was used was setting type Arguments manualy. But maybe that's for the better and AllowedFunctions are better being derrived type instead. – Pavel K Aug 17 '22 at 21:06
  • Updated the solution. Now arguments are derieved from function signatures and generics are used to tie concrete argument type and concrete function type in caller() function. – Pavel K Aug 18 '22 at 16:31