thisisrandy
thisisrandy

Reputation: 3075

Why do two variables of the same type behave differently depending on how they were produced?

Why, given that both nums and nums2 are of type (0 | 1 | 2)[], do tuple and tuple2 have different types?

// const nums: (0 | 1 | 2)[]
const nums: (0 | 1 | 2)[] = [];

// let tuple: (0 | 1 | 2)[]
let tuple = [nums[0], nums[1]];


// const nums2: (0 | 1 | 2)[]
const nums2 = ([] as {num?: 1 | 2}[]).
    map(n => typeof n.num === "undefined" ? 0 : n.num)

// let tuple2: number[]
let tuple2 = [nums2[0], nums2[1]];

Playground

Upvotes: 3

Views: 67

Answers (2)

jcalz
jcalz

Reputation: 328292

This is a consequence of the difference between widening and non-widening literal types, as implemented in microsoft/TypeScript#11126. It's understandably confusing, because IntelliSense does not display these types any differently; it's an invisible attribute of the type. For example, you can't tell if a type displayed as 0 | 1 | 2 is widening or not.


Generally speaking, a literal type that only occurs in an expression (and not in a type) will automatically widen to its corresponding base primitive type (so 0 will become number) when inferred in a place where the value could be reassigned (such as the element of a non-readonly array or tuple). So for this:

const nums = ([] as { num?: 1 | 2 }[]).map(n => typeof n.num === "undefined" ? 0 : n.num)
// const nums: (0 | 1 | 2)[]

The type of nums is an array of widening literal types, because the type 0 comes from the expression 0 and the type 1 | 2 comes from the expression n.num, but nowhere in that map() method call are such types explicitly written as types. And so 0 | 1 | 2 widens to number when inferred in a place that can be reassigned, such as an array literal:

let tuple = [nums[0], nums[1]];
// let tuple: number[]

On the other hand, a literal type that occurs explicitly in a type (not just an expression*) will not automatically widen in a similar situation. So for this:

const nums: (0 | 1 | 2)[] = [];
// const nums: (0 | 1 | 2)[];

The type of nums is an array of non-widening literal types, because the type 0 | 1 | 2 comes from the type annotation for nums. And so 0 | 1 | 2 does not widen to number when inferred in a place that can be reassigned, such as an array literal:

let tuple = [nums[0], nums[1]];
// let tuple: (0 | 1 | 2)[];

If you want a widening literal type to become non-widening, you can add an explicit type somewhere as a hint. Like explicitly typing one of the values in the expression:

const nums = ([] as { num?: 1 | 2 }[]).map(n => typeof n.num === "undefined" ? 0 as 0 : n.num)
// const nums: (0 | 1 | 2)[]
let tuple = [nums[0], nums[1]];
// let tuple: (0 | 1 | 2)[];

or explicitly specifying the type parameter for the call to map()

const nums = ([] as { num?: 1 | 2 }[]).map<0 | 1 | 2>(n => typeof n.num === "undefined" ? 0 : n.num)
// const nums: (0 | 1 | 2)[]
let tuple = [nums[0], nums[1]];
// let tuple: (0 | 1 | 2)[];

or any of the methods discussed in this comment inside microsoft/TypeScript#12267.

Playground link to code

Upvotes: 2

J&#233;r&#233;mie RPK
J&#233;r&#233;mie RPK

Reputation: 317

I think TypeScript reads:

let partOfSomeNums2 = [someNums2[1], someNums2[0]];

like

let partOfSomeNums2 = [0, 1];

In the second line: partOfSomeNums2 is a number[], just like the first line.

You can use:

let partOfSomeNums2 = [...someNums2.slice(1, 2), ...someNums2.slice(0, 1)];

Or:

let partOfSomeNums2: (0 | 1 | 2)[] = [someNums2[1], someNums2[0]];

I think your someWorkingFunction in your playground works because the type 0 | 1 | 2 is directly extracted from an enum, in contrary of your not working code, where the value 0 is hard coded.

EDIT

I would use this solution: Playground

Using this type: type SmallNumber = 0 | 1 | 2; And then, use this type in your mapping function:

const someNums2 = someObjects2.map((obj): SmallNumber =>
    typeof obj.num === "undefined" ? 0 : obj.num
);

Upvotes: 1

Related Questions