microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.76k stars 12.46k forks source link

Poor tuple support #20899

Closed Zarel closed 6 years ago

Zarel commented 6 years ago

TypeScript Version: 2.7.0-dev.20171226

Code

function getPromise(): Promise<[string, number]> {
    return Promise.resolve(['', 0]);
}

The above is an example, but it's not the only example of where TypeScript has trouble with tuples.

Others include:

let a: [string, number] = ['', 0];
let b: [string, number] = a.slice();

The above one is harder, but it'd be great if array.slice() and array.slice(0) could be used to clone tuples.

https://github.com/Zarel/Pokemon-Showdown/blob/2b5654e307d7d65ee16c8bf55961b352c97c96e9/users.js#L1438

The above linked example disappeared when I tried to construct a simple equivalent testcase:

let a = null as [string, string, object][] | null;

if (a) {
    a.push(['', '', {}]);
} else {
    a = [['', '', {}]];
}

But if you care to clone that repo, checkout that commit, and delete the // @ts-ignore, you'll get a similar error message as the others, where the tuple degrades to an array.

Expected behavior:

No errors

Actual behavior:

test1.ts(2,2): error TS2322: Type 'Promise<(string | number)[]>' is not assignable to type 'Promise<[string, number]>'.
  Type '(string | number)[]' is not assignable to type '[string, number]'.
    Property '0' is missing in type '(string | number)[]'.
test1.ts(2,5): error TS2322: Type '(string | number)[]' is not assignable to type '[string, number]'.
  Property '0' is missing in type '(string | number)[]'.
users.js(1438,4): error TS2322: Type '(string | Connection)[][]' is not assignable to type '[string, string, Connection][] | null'.
  Type '(string | Connection)[][]' is not assignable to type '[string, string, Connection][]'.
    Type '(string | Connection)[]' is not assignable to type '[string, string, Connection]'.
      Property '0' is missing in type '(string | Connection)[]'.
DanielRosenwasser commented 6 years ago

I think the general case you're seeing is a design limitation of our analyses not seeing far enough where these values get used. But see #4988 for more relevant discussion on the idea of slice() returning this.

Zarel commented 6 years ago

I think I understand the problem. When inferring type, tuples should degrade to arrays, so in situations like this:

let a = [1, 2];

a is inferred to be number[] rather than [number, number], so that later, a.push(3); isn't an error.

I think a simple workaround might be to prefer tuples to arrays of multiple type.

So [1, 2] would be inferred as number[] rather than [number, number], but [1, ''] would be inferred as [number, string] rather than (number | string)[].

It might be different from others, but in my experience, multi-type tuples are much more common than multi-type arrays, and I would rather specify the latter explicitly than the former.

I guess the biggest problem with that approach might be backwards compatibility, though...

michaeljota commented 6 years ago

but [1, ''] would be inferred as [number, string] rather than (number | string)[].

I think there is no way for the compiler to know that you what to do a tuple instead of an array. Either case, you could check the stricter tuple PR, that probably will be available in next release. #17765. Still, I don't think the compiler should mark a tuple a initialized array.

Zarel commented 6 years ago

In hindsight, I think mutable array literals should be pretty rare. If I write a non-empty array literal, I usually want it to be either immutable or a tuple.

The times I want to initialize a non-empty array and keep it mutable are rare enough that I'd probably rather have TypeScript infer it's a tuple by default and explicitly type them if I want them mutable, than have TypeScript infer it's mutable by default and explicitly type them if I want them to be a tuple.

Zarel commented 6 years ago

Perhaps a compiler option to infer all non-empty array literals as tuples by default?

sandersn commented 6 years ago

@Zarel mutable array literals are rare but we are constrained by backward compatibility for those rare cases (see #17765 for exactly such a case that we ran into with firebase). There may be other reasons to make array literals immutable, but tuple usage is also quite rare [1], so we would need other, stronger motivations.

[1] As far as I can tell from sources like definitely typed and our user-code tests. Javascript's object literal support is close enough to tuples that it seems like most people don't bother.

Zarel commented 6 years ago

I know, which is why I'm asking for it to be behind a compiler flag.

RyanCavanaugh commented 6 years ago

A flag isn't a great choice because the choice of tuple vs not is really a global decision - some arrays should be tuples, some aren't. In any case this problem isn't worth doubling the configuration space over.