Union of singletons vs singleton of union, one-element array, single-value array, singleton.
Intersection of 1-tuples should be the same as 1-tuple of the intersection.
As a 1-tuple, or single-element array, contains only one wrapped value, the exact same type operations that are applied to them also apply to their elements. That is, saying "a 1-tuple containing either A or B" is the same as saying "either a 1-tuple containing A, or a 1-tuple containing B". This applies both to type unions and type intersections.
This all should be true in TypeScript:
[A|B|C] === [A|B] | [C] === [A] | [B] | [C]
[A|B] & [C] === [(A|B)&C] === ([A]|[B])&[C]
// where A === B means both A is assignable to B and B is assignable to A
[A|B|C] ⊂ Array<A|B|C>
[A|B|C] ⊂ A[] | B[] | C[]
// where A ⊂ B means A is assignable to B, but B is not assignable to A
type value = string | number;
type array = string[] | number[];
function(a: value | array)
{
const arr = Array.isArray(a) ? a : [a]; // this should work
}
See Use Cases, I couldn't come up with another example that would be different enough to be worth mentioning.
My suggestion meets these guidelines:
This is at best only true under a full unification algorithm. As-is, this would be a substantial breaking change since:
const arr1: [string] | [number] = ["0" as any];
const arr2: [string | number] = ["0" as any];
const k1 = arr1.filter(e => e === "0");
const k2 = arr2.filter(e => e === "0");
You're right, didn't think of that. Still I think the code in Use Cases should work, I'm just not sure how to achieve that reasonably. Could we, for example, infer the type of 1-tuple literals to be either union-of-tuple or tuple-of-union depending on the context?
declare let value: string | number;
let a = [value]; // [string | number] as before
let b: [string | number] = [value]; // works
let c: [string] | [number] = [value]; // also works
Or is the idea dumb on a fundamental level and should I abandon it and rather summon as any every time I'd use something like that?
The code in "Use Cases" already works?
Anyway I think the "right fix" is a sort of combinatorial assignability reasoning that considers all possible expansion forms of a source type and sees if they have a corresponding valid assignment target. IOW in theory [string | number, string | number] should be assignable to [string, string] | [number, number] | [string, number] | [number, string].
Okay, I did too much posting and not enough testing :)
The code I had problem with was this:
function fn(patterns: string | string[] | RegExp | RegExp[]): void
{
// I have to uncomment the assertion or else it throws
if (!Array.isArray(patterns)) patterns = [patterns] // as [string]|[RegExp];
}
But it seems that I'd better close this issue and open a more specific one about that precise problem.
The inferred type of [patterns] says this is allowed:
function fn(patterns: string | string[] | RegExp | RegExp[]): void
{
if (!Array.isArray(patterns)) {
// k: Array<string | RegExp>
const k = [patterns];
// therefore these are both valid
k.push("");
k.push(/expr/);
// Writes a heterogeneous array where a homogeneous one is expected
patterns = k;
}
}
Somewhat awkwardly, this is legal 🙃
patterns = typeof patterns === "string" ? [patterns] : [patterns];
This is probably fixed by https://github.com/Microsoft/TypeScript/pull/30779 since a union of tuples should be a discriminated union.