Reputation: 3075
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]];
Upvotes: 3
Views: 67
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.
Upvotes: 2
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