Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1-tuple of union should be the same as union of 1-tuples #30895

Closed
5 tasks done
m93a opened this issue Apr 12, 2019 · 7 comments
Closed
5 tasks done

1-tuple of union should be the same as union of 1-tuples #30895

m93a opened this issue Apr 12, 2019 · 7 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@m93a
Copy link

m93a commented Apr 12, 2019

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:

  • 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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Apr 12, 2019
@RyanCavanaugh
Copy link
Member

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");

@m93a
Copy link
Author

m93a commented Apr 12, 2019

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?

@RyanCavanaugh
Copy link
Member

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].

@m93a
Copy link
Author

m93a commented Apr 12, 2019

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.

@m93a m93a closed this as completed Apr 12, 2019
@RyanCavanaugh
Copy link
Member

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;
    }
}

@RyanCavanaugh
Copy link
Member

Somewhat awkwardly, this is legal 🙃

        patterns = typeof patterns === "string" ? [patterns] : [patterns];

@weswigham
Copy link
Member

This is probably fixed by #30779 since a union of tuples should be a discriminated union.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

3 participants