Repository navigation
Proposal: strict and open-length tuple types #6229
Description
Activity
- addedSuggestionAn idea for TypeScriptAn idea for TypeScript
on Dec 23, 2015 Igorbek commented
on Jan 4, 2016 ContributorAuthorMore actionsAny thoughts on this? Do you think it could be proposed?
- addedIn DiscussionNot yet reached consensusNot yet reached consensus
on Jan 8, 2016 - changed the title
[-]Suggestion: stricter tuple types[/-][+]Proposal: strict and open-length tuple types[/+]on Feb 1, 2016 JsonFreeman commented
on Mar 5, 2016 ContributorMore actionsYou'd still have the problem of calling
pushorpopon the tuple. Or these subtler variants:array[array.length] = 0; array.length--;
just a personal observation, as of now the following way of doing tuples gets more predictable results than the official tuples that are mostly arrays
interface T2<a, b> { 0: a; 1: b; } function t2 <a, b>(one: a, two: b) : T2 <a, b> { return <any> [one, two]; }Reacted by Christoph Müller, Aleh Kashnikau, SlurpTheo, Thomas Rognon, Carl Patenaude-Poulin, RebendaJiri and Jacob SmithIgorbek commented
on Mar 5, 2016 ContributorAuthorMore actionsJason Freeman (@JsonFreeman) hm, fair. However we have similar things that can cheat type system. Such as array variance.
var animals: Animal[] = dogs; animals.push(cat); dogs[dogs.length-1].woof(); // boom
BTW,
array.length--breaks existing rules too.Option 1 - restrict such operations on fixed-length tuples. So let's say if array boundaries wasn't proven - give an error.
Option 2 - allow to shoot the leg, as we do in other cases.By the way, 👍 for this proposal. It seems to me a necessity for solving the problem with variadic types. No matter how you try to solve the variadic problem (I've seen several ideas already), this comes up and gets in the way every single time.
Ability to distinguish strict tuples and open tuples looks reasonable.
Igor Oleinikov (@Igorbek) My opinion on that:
- Option 1: Always require
<Animal[]> <any> dogsfor that. I've used that trick before to cheat the type system (it couldn't tell I was actually doing something that was type-safe a few times). Fixed-length tuples should only have a subset of the Array operations (push/pop should not exist on the type even though they technically exist, for example). - Option 2: I'd be okay with open-ended tuples being a true subtype of arrays.
Reacted by Shad Sterling- Option 1: Always require
@isiahmeadows
Fixed-length tuples should only have a subset of the Array operations
Agree.
Moreover, it seems to me that fixed-length tuples are needed only in read-only scenarios...Anatoly Ressin (@Artazor) There are times when it's nice to be able to write to a tuple. It's not frequent, but it's occasionally helpful. I would be okay with
copyWithinwith limited semantics. The biggest reason I'm interested in fixed-length tuples is that it would help solve the variadic problem withbindtremendously (that combined with non-strict type checking).@isiahmeadows
Imagine, that[T1,T2,T3]means strict tuple. Let's try to write a problematic code:var a: [number, boolean] = [1, true] //strict var b: [number, boolean, number, boolean] //strict b = [...a, ...a] // ok a[0] = 2; // ok (statically) b = [...a, ...a] // still ok a[a[0]] = 3; // can we prevent this at compile time? (doubt) b = [...a, ...a] // oops!
I feel it should be restricted to n-tuples of just a single type. As for indexed access, it should be unsafe, because otherwise it's much more complicated for the compiler, and if you're resorting to this over plain objects in most cases, either the code probably already smells of feces (little the language can do to help you here), or you know what you're doing, and need that indexed access for performance reasons (e.g. an 8-entry table, which the compiler will infer).
As for varying types, it should be read-only, but unsafe read access is pretty much the only way to do it in practice. Otherwise, it's unnecessary boilerplate outside of destructuring. Matter of fact, in many of these kinds of cases, Haskell prefers crashing over returning a
Maybe, since it's far faster and chances are, you probably already have an idea whether your index is within range.Remember, you can only do so much statically - some things are literally undetectable until runtime, no matter how powerful your type system is.
JsonFreeman commented
on Mar 6, 2016 ContributorMore actionsI agree with the general sentiment of wanting fixed length tuples. The reason I am worried about the length of the tuple not being perfectly enforceable is that if it is used to solve the variadic bind typing, you won't just get a tuple/array of the wrong length. You'll get a function with the wrong number of arguments! For some reason that seems a lot worse to me than a tuple of the wrong length, or even arguments of the wrong types.
29 remaining items
Well, Ramda typings also just ran into this issue, typed-typings/npm-ramda#173 (comment). Specifically, after a function overload asking for a higher-length tuple failed, it fell through to an overload asking for a unary tuple, which then matched, going against desired behavior. Not seeing clear alternatives (based on overloads) that could do without this.
KiaraGrouwstra commented
on Aug 13, 2017 ContributorMore actionsPotential solution, tie tuples to a new
Tupleinterface (sub-typingReadOnlyArray), following the suggestion by Mohamed Hegazy (@mhegazy) at #16503 (comment), such as to specify known length. I'd imagine the distinctlengthliterals would prevent one from assigning higher-length tuples to lower-length ones, as suggested here.interface Tuple<TLength extends number, TUnion> extends ReadonlyArray<TUnion> { length: TLength; }
The obvious question here seems whether ending this assignability would break much in practice. Seems worth finding out.
Then again though, in other areas like implicit JS casts like
Number->StringTS's stance appears to have been that explicit conversions beat implicit magic, and I suppose it might not be unreasonable to extend that reasoning to this tuple case as well.KiaraGrouwstra commented
on Aug 13, 2017 ContributorMore actionsI've just opened a WIP PR based on the explicit
lengthidea at #17765. I think I'm half-way, but feel a bit stuck about how to properly get the tuples to derive from this interface; input welcome.KiaraGrouwstra commented
on Aug 19, 2017 ContributorMore actionsUpdate: got it to work. Using a flag for those concerned about breaking change, so should be win-win.
Reacted by Aluan Haddad, SlurpTheo, HE Shi-Jun, Andrew Schmadel, Igor Oleinikov and Thomas Rognon- addedFixedA PR has been merged for this issueA PR has been merged for this issueand removedIn DiscussionNot yet reached consensusNot yet reached consensus
on Nov 8, 2017 - addedBreaking ChangeWould introduce errors in existing codeWould introduce errors in existing code
on Nov 8, 2017 @tycho01 I used your trick with length here and I was able to define a type for arrays of generic fixed size. It is a bit hacky, but it seems to work in some common use cases. I do not know if this application is known or if it can be reduced to your work without defining a new interface as I did.
KiaraGrouwstra commented
on Mar 28, 2018 ContributorMore actionsMarco (@mstn): I hadn't tried that -- I've no idea how your
0workaround managed to beat the type widening issue!
That said, it seems to work also as the simplifiedtype FixedSizeArray<N extends number, T> = { 0: any, length: N } & ReadonlyArray<T>?Reacted by ShinigamiYes, you are right. Actually, the default 0 for M yields nothing else but { 0: any }!
The trick works only for tuple and not for the corresponding objects. Moreover, it works only with
0(or a sequence0,1, ...) and not with non "sequential" keys. It fails for tuple types, of course.Is it a bug or a feature?
type A = { 0: any }; let a1: A = ['a', 'b']; // ok let a2: A = { 0: 'a', 1: 'b' }; // error type B = { 1: any }; let b1: B = ['a', 'b']; // error let b2: B = { 0: 'a', 1: 'b' }; // error type C = { 0: any, 1: any }; let c1: C = ['a', 'b', 'c']; // ok let c2: C = { 0: 'a', 1: 'b', 2: 'c' }; // error type D = [any]; let d1: D = ['a']; // ok let d2: D = ['a', 'b']; // error
If we think in Javascript, an array is an object with sequential numerical keys. Hence, expressions like
a1orc1are a sort of upcasting. The Typescript compiler is smart enough to understand it! So I think it should be a feature. What do you think?Reacted by kiara and Bao Bo- locked and limited conversation to collaborators
on Jul 25, 2018
Update: converted to proposal.
Background
Currently, tuples are arrays that are restricted in minimum length, but not in maximum:
This makes harder to predict types or errors in some scenarios:
There also might be difficult to adopt variadic kinds, especially in type construction:
Proposal
(1) Restrict tuple instance to match exact length
(2) Introduce open length tuple types
Open-length tuple types will be the same as tuple types are now.
(3) Improve contextual type inference for spread operators on tuples
Related issues
This addresses:
Disadvantages
This is definitely a breaking change.