Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Typescript infer 'never' instead of intersection type?

Tags:

typescript

Given the following example:

interface Data {
    name: string;
    value: number;
}

const data :Data = {
    name: 'name',
    value: 1,
}

const updateValue = (key: keyof Data, value: string | number): void => {
    data[key] = value;
};

link to ts-playgound

Typescript shows the following error:

Type 'string | number' is not assignable to type 'string & number'.
  Type 'string' is not assignable to type 'string & number'.
    Type 'string' is not assignable to type 'number'.

Which is clear and understandable. However if I add a union type to the interface like so:

type MultiType = 'x' | 'y';

interface Data {
    name: string;
    value: number;
    multiType: MultiType | null;
}

const data :Data = {
    name: 'name',
    value: 1,
    multiType: null,
}

const updateValue = (key: keyof Data, value: string | number): void => {
    data[key] = value;
};

link to ts-playgound

I get the following error:

Type 'string | number' is not assignable to type 'never'.
  Type 'string' is not assignable to type 'never'.

Typescript accepts it if I use the intersection type string & number & MultiType but it also accepts never.

This seems inconsistent to me. Is this maybe a bug?

like image 386
AndiH Avatar asked Jul 25 '19 08:07

AndiH


People also ask

When would you use an intersection type instead of a union type?

Intersection types are closely related to union types, but they are used very differently. An intersection type combines multiple types into one. This allows you to add together existing types to get a single type that has all the features you need.

What is infer in TypeScript?

Using infer in TypeScript The infer keyword compliments conditional types and cannot be used outside an extends clause. Infer allows us to define a variable within our constraint to be referenced or returned.

What is an intersection type in TypeScript?

An intersection type creates a new type by combining multiple existing types. The new type has all features of the existing types. To combine types, you use the & operator as follows: type typeAB = typeA & typeB; Code language: TypeScript (typescript)

How do you handle a union type in TypeScript?

TypeScript Union Type Narrowing To narrow a variable to a specific type, implement a type guard. Use the typeof operator with the variable name and compare it with the type you expect for the variable.


2 Answers

string & number is equivalent to never, and so is string & number & (MultiType | null). There are no values which are both a string and a number, so no values satisfy string & number or string & number & AnythingElse.

Right now the latter explicitly reduces to never because it contains unions of these equivalent-to-never types, which is really ugly to leave like that. Specifically, the compiler distributes intersections over unions, so

string & number & ('x' | 'y' | null)

becomes

(string & number & 'x') | (string & number & 'y') | (string & number & null)

That type isn't particularly enlightening to people, so the compiler checks that each of those union constituents is equivalent to never and reduces the type to

never | never | never

which is just

never

as you saw.


So why doesn't string & number by itself get immediately reduced to never? Well originally the idea was that it would help people understand where the bug in their code came from, since string is not assignable to number is more enlightening that string is not assignable to never.

Unfortunately while string & number is equivalent to never in terms of what values are assignable to and from it, in TS3.5 and below the compiler doesn't always treat them the same, which is confusing.

Therefore it looks like, starting in TS3.6, empty intersections like string & number will be reduced to never explicitly. Once TS3.6 is released, your above code will act the same in both cases, and you'll get the string | number is not assignable to never error.


Okay, hope that helps; good luck!

like image 158
jcalz Avatar answered Sep 27 '22 20:09

jcalz


Two things :

  • It's only logical that TypeScript doesn't accept string | number as type for your value parameter, because all of the following :

    • string | number is not assignable to string because number is not assignable to string in case the key is 'name'

    • string | number is not assignable to number because string is not assignable to number in case the key is 'value' (both of there were already true in your first example)

    • string | number is not assignable to MultiType | null because both number and string are not assignable to 'x' | 'y' | null in case the key is 'multiType'

For some reason however, in your first example TypeScript simply stops on the first "wrong" case and gives you this error (Even though there are really two things wrong already).

In the second case, you could have seen the same error message because the incompatibility of types is still there, it seems like the inference goes a little deeper and tells you that the problem is deeper than this. As to why is the error message formatted like this, I don't really know and I guess it would require going deeper into how are things inferred by the compiler here. In the end, the compiler is right, but the error message could be clearer.


For your second question, the never type documentation says :

The never type is a subtype of, and assignable to, every type

So that's why you can specify your value to be of type never, and assign it to your data. Because it will never happen.

like image 24
Kewin Dousse Avatar answered Sep 27 '22 21:09

Kewin Dousse