Home > Enterprise >  TypeScript: union and intersection of functions behave unexpectedly
TypeScript: union and intersection of functions behave unexpectedly

Time:05-02

In this code, example1 and example2 are confusing me:

type F1 = (a: string, b:string) => void;
type F2 = (a: number, b:number) => void;

const example1: F1 & F2 = (a: string | number, b: string | number) => {}
example1("Hello", "World")
example1(1, 2)
// example1("Hello", 2) // Error! number is not assignable to parameter of type string... (and vice versa)

const example2: F1 | F2 = (a: string | number, b: string | number) => {}
// example2("Hello", "World") // Error! Argument of type string is not assignable to parameter of type never
// example2(1, 2) // Error! Argument of type number is not assignable to parameter of type never
// example2("Hello", 2) // Error! Argument of type string is not assignable to parameter of type never

// const example3: number | string = true // Error! Type Boolean is not assignable to type number | string
const example4: number | string = 1
const example5: number | string = "foo"

// const example6: {a: string} & {b: number} = {a: "foo"} // Error! Type '{ a: string; }' is not assignable to type '{ a: string; } & { b: number; }'. 
// Property 'b' is missing in type '{ a: string; }' but required in type '{ b: number; }'.
const example7: {a: string} & {b: number} = {a: "foo", b: 5}

To me it seems like the operators in example1 and example2 are behaving the opposite way from the others. Here's how I would expect these examples to work:

const example1: F1 & F2 = (a: string | number, b: string | number) => {}
// example2("Hello", "World") // Error! Argument of type string is not assignable to parameter of type never
// example2(1, 2) // Error! Argument of type number is not assignable to parameter of type never
// example2("Hello", 2) // Error! Argument of type string is not assignable to parameter of type never

const example2: F1 | F2 = (a: string | number, b: string | number) => {}
example1("Hello", "World")
example1(1, 2)
// example1("Hello", 2) // Error! number is not assignable to parameter of type string... (and vice versa)

It would also make sense to me if example1 didn't even compile, since "type of string !== type of number".

Why isn't this working as expected?

CodePudding user response:

With these types,

type F1 = (a: string, b:string) => void;
type F2 = (a: number, b:number) => void;

and this declaration of example1,

const example1: F1 & F2 = (a: string | number, b: string | number) => {}

example1 has declared type F1 & F2, so it can be called both as a function of two strings and as a function of two numbers. But it can't be called with a mix of the two arguments. The function value you assigned to it could, but F1 & F2 is strictly a supertype of (a: string | number, b: string | number) => void, so we lost information when we assigned to a variable with a static supertype, in the same way that assigning the number 3 to a variable of type unknown loses information.

const example2: F1 | F2 = (a: string | number, b: string | number) => {}

The type of example2 is the type of either functions which can be called with string arguments or those that can be called with number arguments. The function you're assigning to it can be called with either, so the assignment is fine.

But we can never call this function. At all. We would have to pass it two arguments, where those arguments are compatible with both the F1 and F2 signatures. F1 expects string and F2 expects number, so we need to pass something that's both a string and a number, i.e. string & number. And string & number is never, the empty type.

The reason the | turns into an & in that second function is due to a little thing called variance. Function arguments are contravariant, so ((a: A1) => B1) & ((a: A2) => B2) is equal to (a: A1 | A2) => B1 & B2 and ((a: A1) => B1) | ((a: A2) => B2) is equal to (a: A1 & A2) => B1 | B2. You can read that Wikipedia page for more details on the math behind it, or write out what the "and" and "or" type means and follow your intuition.

CodePudding user response:

As types are nothing bet sets, It all follows the Set theory.

First, let's understand a crucial behavior of & in type system.

{ a: 'number' } & { b: 'string' } => { a: 'number', b: 'string' }

Both of the constituents types got added as they were independent types.

Keeping this in mind, let's try to prove this behavior and what should be the equivalent result of F1 & F2 and F1 | F2

type F1 = (a: string, b:string) => void;
type F2 = (a: number, b:number) => void;



Using SET Theory

Eq 1.  (A | B) = A   B - (A & B) 
Eq 2.  (A & B) = A   B - (A | B) 

Also, our F1 and F2 are independent type sets, hence

Eq 3. F1 & F2 => F1   F2


Case 1: F1 & F2

> F1   F2     ...(By using eq 3)
> (string, string) => void   (number, number) => void 
> Basically means we can only call F1 and F2 with their own types (not their union one)

 

Case 2: F1 | F2


> F1   F2 - (F1 & F2)    ....(By Using Eqn 1)
> F1   F2 - (F1   F2)    ....(By using Eqn 3)
> never (as everything cancels out)


  • Related