Home > Enterprise >  Typescript: `|` of function types combines their inputs like an `&` instead of like an `|`
Typescript: `|` of function types combines their inputs like an `&` instead of like an `|`

Time:08-18

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
  • Related