Repository navigation
suggestion: explicit "tuple" syntax #16656
Description
Problem
I'm writing this after encountering (what I think is) #3369. I'm a noob to TS, so I apologize for any misunderstandings on my part. The behavior the lack of type inference here:
interface Foo {
bar: [number, number];
}
interface McBean {
baz: Foo;
}
// error: McMonkey does not implement McBean
class McMonkey implements McBean {
baz = {
bar: [0, 1]
};
}
// vs
// no error
class McMonkey implements McBean {
baz: Foo = {
bar: [0, 1]
};
}Because array literals are (correctly) inferred to be arrays, TS is limited in its ability to infer "tuple". This means there's an added overhead to working with tuples, and discourages use.
As I see it, the problem is that a tuple is defined using array literal notation.
A Conservative Solution
Adding a type query (?) such as tuple (.e.g tuple) would be better than nothing:
class McMonkey implements McBean {
baz = {
bar: <tuple [number,number]> [0, 1]
};
}...but this is still clunky, because you'd have to use it just as much as you'd have to explicitly declare the type.
A Radical Solution
There's a precedent (Python) for a tuple syntax of (x, y). Use it:
interface Foo {
bar: (number, number);
}
interface McBean {
baz: Foo;
}
class McMonkey implements McBean {
baz = {
bar: (0, 1)
};
}Obvious Problem
The comma operator is a thing, so const oops = 0, 1 is valid JavaScript. 0 is just a noop, and the value of oops will be 1. Python does not have a comma operator (which is meaningful in itself).
I've occasionally used the comma operator in a for loop, similar to the MDN article. Declaring var's at the top of a function is/was common:
function () {
var a, b, c;
}Parens are of course used in the syntax of loops and conditionals, as well as a means of grouping expressions.
A nuance of Python's tuples are that they must contain at one or more commas, which could help:
foo = (0) # syntax error or just `0`; can't recall
foo = (0,) # valid tuple...or it could actually make matters worse, because (0, ) is an unterminated statement in JavaScript.
That all being said, using the suggested Python-like syntax, it seems difficult but possible for the compiler to understand when the code means "tuple" vs. when it doesn't.
I'd be behind any other idea that'd achieve the same end. I don't know how far TS is willing to diverge from JS, and I imagine significant diversions such as the above are not taken lightly.
(I apologize if something like this has been proposed before; I searched suggestions but didn't find what I was looking for)
Activity
DanielRosenwasser commentedon Jun 20, 2017
Very related to #10195
boneskull commentedon Jun 20, 2017
Similar syntax suggestion; different purposes.
As a former TypeScript-hater & JavaScript purist, I can tell you the verbosity was a turnoff. Expanding the inference capabilities of TS -- and subsequently featuring them! -- would make it easier to approach & use for many like myself.
jcalz commentedon Jun 20, 2017
Just FYI, your conservative solution already works if you leave off the word
tuple:but I understand you'd rather put typescript through the Annotation-Off Machine.
arturkulig commentedon Jun 21, 2017
This is valid TS:
It can be dealt with, but requires caution and discipline, which ideally TS should not require for types.
Proposal: infer literal types from literal array notation
boneskull commentedon Jun 22, 2017
Joe Calzaretta (@jcalz) Right; perhaps then what I should have suggested is a
tuplebuilt-in type, likeobject.When speaking of annotations, without a doubt, the best types are the types without.
masaeedu commentedon Jun 27, 2017
Perhaps we coud break a portion of the the
Arrayinterface out into aReadOnlyArrayinterface, whichArraywould extend, and provide some syntax (I have no preference here, as an example perhaps`["foo", "bar"]or<readonly>["foo", "bar"]), to createReadOnlyArrayliterals. Since the elements of aReadOnlyArrayare not mutable positions (by virtue of the limited API it exposes), they may safely be inferred as literals.This breakup of
Arraywould be necessary anyway for work on co/contravariance.RyanCavanaugh commentedon Jun 27, 2017
Asad Saeeduddin (@masaeedu) https://github.com/Microsoft/TypeScript/blob/master/lib/lib.d.ts#L986
masaeedu commentedon Jun 27, 2017
Ryan Cavanaugh (@RyanCavanaugh) Awesome! That means we're almost there, but
jis still not being inferred as[1, 2, 3]. That's probably because the expression[1, 2, 3]widens tonumber[]and irrevocably throws away the type information.It'd be nice if the compiler was happy with the following:
tvald commentedon Jul 13, 2017
You can simulate a built-in
Tupletype via the following:Note that the longer definitions do need to come first.
tvald commentedon Jul 13, 2017
Built-in language support for an explicit
Tuple(ortuple) type would be highly desirable.Aggressive conversion of tuples to arrays is a common problem. See also: #3369, #8276, #15071, #16389, #16503, #16700... more
At present, it's hypothetically possible to explicitly annotate a tuple everywhere it would be converted to an array, but that's only feasible if (a) you can/have imported the element type definitions, and (b) they aren't a mess of unioned or anonymous types.
KiaraGrouwstra commentedon Aug 6, 2017
In that #16389 I argued to have
constinferconst x = [1, '', {}, 1]to be of type[1, '', {}, 1](as opposed to[number, string, object, number]mentioned in theTuplesuggestion above), and ditto for objects, mirroring howconstalready infers literals for primitives.Retaining all info this way matters for e.g. Ramda's
pathfunction, which would use the literals to navigate a given structure.The primary counter-argument found there was granular types would break
Array.prototype.push, which I believe could be fixed with apushoverload on a tuple interface inlib.d.ts.Am I missing any flaws there?
aluanhaddad commentedon Aug 6, 2017
@tycho01 that's kind of an awesome idea and it would open up so many scenarios that are valuable but at the same time it would be a massive breaking change.
KiaraGrouwstra commentedon Aug 6, 2017
Aluan Haddad (@aluanhaddad): perhaps if we know what else might break we could tackle them one by one? If the workarounds could be as simple as adding tuple interfaces to
lib.d.ts, then great.aluanhaddad commentedon Aug 6, 2017
For tuples that might be all it would take, and could quite well be worth it at that point but I guess I was thinking of it in a more generalized form that would apply to the top level properties of object literals as well.
In general I don't use classes and favor a style making heavy use of standard functions, object literals, arrays, and
{...x}. A big pain point is not getting literal type inference on any object literal properties. Since I don't reassign them I would like them to be literal types so that I can take advantage of CFA (especially exhaustiveness checking) and improved type-inference. I probably just need to use more helper libraries like Ramda 😁2 remaining items
KiaraGrouwstra commentedon Aug 13, 2017
Trying that compiler flag at https://github.com/tycho01/TypeScript/tree/16656-granularConst. Haven't fully figured it out yet though.
KiaraGrouwstra commentedon Aug 14, 2017
Progress: PR at #17785. still testing more though.
kpdonn commentedon Mar 18, 2018
So while creating a related suggestion (#22679) I stumbled upon a way to accomplish this in current versions of typescript that I haven't seen before. Fair warning though that it is just taking advantage of an implementation detail so it might stop working in the future.
Playground link
or the example from the first post in this issue:
Playground Link
crdrost commentedon Jun 8, 2018
The proposed "radical solution" would mean that in certain circumstances TypeScript is no longer a superset of JavaScript, so it is probably a no-go.
However we could maybe steal F#'s array syntax.
Note that there is a very common circumstance where I'd like to use this, namely map initializers. Right now I have to write in several places,
Note that I am not using the array initializer because it makes the code much less readable and possibly might hide type errors with the coercive weight of
as:note that there's two sources of noise here, the intrinsic noise of the
mapand the unnecessary noise of telling TypeScript that I meant to write a[string, object]tuple not a(string | object)[]array, and either one of them is readable on its own (the type declaration is no more complex than the type above) but it's that they have to be joined together which makes this look jarring. So the above keeps the type declaration in exchange for a loop.If we allow
[| ... |]for a tuple constructor, we could do the reverse:Note that
|being a binary operator cannot legally appear directly before]or after[so it should preserve the superset-ed-ness?achankf commentedon Jun 22, 2018
Hi. This is going to overlap with CR Drost (@crdrost)'s point about the map function, but I'll voice my concern here.
I am working on a pet project which uses React and Immutable.js. Sometimes Typescript doesn't seem to infer tuples, so I found writing in functional-programming style pretty cumbersome. In the context of React, I often use map and reduce to create updated copies of new states. For example, I would like to use reduce (and map) like this...
The compiler rejects it because c1's type is a union of every values' types instead of number. To get around it, I need to explicitly type the tuples twice, like so...
However, I don't want base to live beyond reduce's scope. Alternatively, I use an object as the accumulator, but the left-hand-side seems noisy, due to naming and the linter's no-shadow-variable rule.
Unless there's a way to explicitly mark tuples, it is very tempting to write imperatively with for-of loops and forgo immutability, especially when partitioning data, which often need intermediate results to be carried over for later uses.
crdrost commentedon Jun 25, 2018
Alfred Chan (@achankf) while we're waiting for this we may want to use tvald's workaround above, which can fit nicely into a module that you can import where needed. We can also avoid overloading a single function word if that raises hairs...
Similar with
pair<A, B>(a: A, b: B)andtriple<A, B, C>(a: A, b: B, c: C).RyanCavanaugh commentedon Aug 19, 2019
I believe this is solved or at least sufficiently addressed with
as const. Thoughts?pelotom commentedon Aug 19, 2019
Ryan Cavanaugh (@RyanCavanaugh) unfortunately most libraries exporting pure functions that operate on arrays or tuples are not rigorous about declaring their inputs
readonly, e.g.so
as constdoesn't play well with them:So I've had to keep this utility around:
rauschma commentedon Dec 28, 2019
Ryan Cavanaugh (@RyanCavanaugh)
as constworked well in my case (where I needed to pass on a complicated mapped type).Possibly: Allow
as tupleas an alternative syntax when the operand is an Array. Rationale: Expresses the intent better.csvn commentedon Mar 10, 2020
Ryan Cavanaugh (@RyanCavanaugh) I regularly use functional approaches, e.g. when mapping/filtering objects with
Object.entries/Object.fromEntries, and I had hopedas constcould be to avoid type widening of tuple to array. In many cases I've found though that thereadonlyarray type prevents me from passing it to other functions, and as such often requires to explicitly type out the tuple type:Axel Rauschmayer (@rauschma)'s suggestion is one I've frequently wished for, or an alternative to
as constthat just disables type widening, but does not addreadonlymodifier:devinrhode2 commentedon Dec 28, 2020
Christian (@csantos42) I'm in that exact same situation, and would also love an
as exactassertion. Ideally TS didn't just throw away the type information in these situations.gausie commentedon May 25, 2021
I would really love this feature. I always thought
as letmight communicate well the difference between a tuple being readonly withas constvs being mutable.MichaelMitchell-at commentedon Dec 1, 2022
The originally posed problem can be somewhat addressed by the new
satisfiesoperator:fedetibaldo commentedon May 20, 2025
The records and tuple proposal has been withdrawn. Is
as tupleagain on the table?I definitely prefer it over the
satisfiesapproach as it lets TypeScript infer the type (just as withas const) without making itreadonly(which complicates interoperability with existing code that expects mutable tuples--i.e., the default). On the last point,as letcould be a nice middle ground.