Typescript: 1-tuple of union should be the same as union of 1-tuples

Created on 12 Apr 2019  ·  7Comments  ·  Source: microsoft/TypeScript

Search Terms

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.

Suggestion

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

Use Cases

type value = string | number;
type array = string[] | number[];

function(a: value | array)
{
  const arr = Array.isArray(a) ? a : [a]; // this should work
}

Examples

See Use Cases, I couldn't come up with another example that would be different enough to be worth mentioning.

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.
Working as Intended

All 7 comments

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.

Was this page helpful?
0 / 5 - 0 ratings