Repository navigation
Consistent intersection type with array and tupleΒ #50346
Description
Suggestion
π Search Terms
array, tuple, intersection, covariance, #32926, #33760
β Viability Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
β Suggestion
I think this is rather theoretical approach for the behavior of intersection types and covariance for the consistency.
If this doesn't meet the design policy, feel free to close this.
π Motivating Example
Comprehension check for the intersection type and covariance
With my understanding of TypeScript and set theory, intersection type seems to obey following:
A & B β A
A & B β B
Also covariance/contravariance can alter the direction of such relation.
A β B
If F needs to be covariant:
F<A> β F<B>
then F<A> & F<B> is equivalent to F<A>
If F needs to be contravariant:
F<A> β F<B>
then F<A> & F<B> is equivalent to F<B>
Current behavior of finite/infinite computed property
Now consider the following object types
type Small = {
[k in 'a' | 'b']: number
}
type Big = {
[k in 'a' | 'b' | 'c']: number
}
let small: Small = { a: 1, b: 1 }
let big: Big = { a: 1, b: 1, c: 1 }
// small = big // (OK)
// big = small // (Invalid)This makes sense. Note that this somehow behaves contravariant, while with the following behaves covariant.
type Big = {
[k in string]: number
}Here is the #playground for a glimpse.
What should be T[] & [T]?
It's basically nonsensical to intersect a tuple type with a variable-length array type;
I already read this from #33760 (comment) . But with the theoretical point of view, I just wonder whether this is helpful for the consistent implementation for future or not.
I don't know the implementation detail of this, but along the empirical tests, I think [T] is considered like:
[T] = {
length: 1
"0": T
// and other array's properties
}And T[] resembles:
T[] = {
length: number
[index in number]: T
// and other array's properties
}Currently, with TypeScript v4.7.4, the result is a little bit weird. #playground
type T = number[] & [number]
let t: T[1] // ??? no error
let len: T["length"] // 1Is this because 0 is not computed property? How about [T, T] or [T, T, T]?
π» Use Cases
Literal type 0 should be considered as finite subset of number, and by "Current behavior of finite/infinite computed property", the computed property of index, should be narrowed into 0. Note that length is already narrowed into 1.
// suggested behavior
type T = number[] & [number]
let t: T[1] // should be error or never
let len: T["length"] // 1I understand that this might impractical, and nobody do number[] & [number]. But I'm not sure that, there is no guarantee for other complex composition never make such result. I hope this insight would be helpful for TypeScript team.
Activity
RyanCavanaugh commentedon Aug 18, 2022
I think this goes back to what
βis understood to mean. One viewpoint -- not the only one you could have -- is that if an operation is valid onB, andA β B, then that same operation must be valid onA.T[1]is valid whenTisnumber[], so it must be valid onnumber[] & [number]The same subtyping/subsetting behavior might be observed on the indexing expression itself -- if
T[number]is legal (which I think it absolutely must be in this case) then it can't be illegal to writeT[1], since1is a subset ofnumber.whzx5byb commentedon Aug 18, 2022
I agree but I think
T[1]should beneverhere, as(A & B)[C]should have the same type asA[C] & B[C].Phryxia commentedon Aug 19, 2022
I understood! As such operation have to be applicable. But its result has to obey the covariance property-
nevermight be the answer.mlacorte commentedon Feb 6, 2023
Chiming in here to leave an opinion.
I'm implementing a type system for an unrelated project, and I've been sanity checking it by seeing how TypeScript handles these cases. I consider the following to be inconsistent behavior:
If differing
array.lengthvalues should produce a type error, then differingObject.keys(object).lengthvalues should too.Given how useful
{ a: 1 } & { b: 2 }is, the decision to ignore key length for the purpose of object intersections seems perfectly justifiable, but I think that decision should be applied consistently to tuples as well, unless there's a reason for the discrepancy that I'm not seeing.