GeezerVison is showing github.com — Proposal: strict and open-length tuple types · Issue #6229 · microsoft/TypeScript
Proposal: strict and open-length tuple types #6229DescriptionUpdate: converted to proposal.
Background
Currently, tuples are arrays that are restricted in minimum length, but not in maximum:
var t1: [number, number] = [1]; // this doesn't work
var t2: [number] = [1, 2]; // this works
This makes harder to predict types or errors in some scenarios:
var t1: [number, string];
var t2 = [...t1, ...t1]; // could be inferred to be [number, string, number, string], but must be inferred as [number, string] (now it's simply inferred as (number|string)[])
var t3: [number, string] = [1, "a"];
t3[2]; // ok, but must be an error
There also might be difficult to adopt variadic kinds, especially in type construction:
function f<...T>(...rest: [...T]): [...T, ...T] {
var rest1: [...T] = [...rest, 1]; // it will be acceptable due to current rules
return [...rest1, ...rest1]; // due to types it seems to be [...T, ...T], but actually is [...T, number, ...T, number]
}
Proposal
(1) Restrict tuple instance to match exact length
var t1: [number, string] = [1, "a"]; // ok
var t2: [number, string] = [1]; // error (existing)
var t3: [number, string] = [1, "a", "b"]; // error (new)
(2) Introduce open length tuple types
Open-length tuple types will be the same as tuple types are now.
var t1: [number, string, ...] = [1, "a", 2, "b"]; // same as current tuples are now
var t2: [number, string, ...(number|string|boolean)[]]; // explicitly type rest elements -- syntax option 1 -- consistent with rest parameters
var t3: [number, string, ...number|string]; // explicitly type rest elements -- syntax option 2
// strict tuple type can be implicitly converted to open length tuple type
var t4: [number, string, string];
var t5: [number, string, ...] = t4; // ok
var t6: [number, ...] = t4; // error, 'number|string' cannot be converted to 'number'
var t6: [number|string, ...] = t4; // ok
var t7: [number, ...(number|string)[]] = t4; // ok
(3) Improve contextual type inference for spread operators on tuples
var t1: [number, string];
var t2: [number, string, number, string] = [...t1, ...t1]; // it's proven now
var t3: [number, ...];
var t4: [string, ...];
var t4: [number, number|string, ...] = [...t3, ...t4]; // this also can be proven
Related issues
This addresses:
Disadvantages
This is definitely a breaking change. Reacted by Asad Saeeduddin, Igor Oleinikov, Meligy, Michael Delaney, Kevin Ryan, phiresky, Andrey Fedorov, EliSnow, Benjamin Zhang, Felipe and 22 more ActivityIgorbek commented on Jan 4, 2016 ContributorAuthor More actions Any thoughts on this? Do you think it could be proposed? changed the title [-]Suggestion: stricter tuple types[/-] [+]Proposal: strict and open-length tuple types[/+] on Feb 1, 2016JsonFreeman commented on Mar 5, 2016 You'd still have the problem of calling push or pop on the tuple. Or these subtler variants:
array[array.length] = 0;
array.length--; zpdDG4gta8XKpMCd commented on Mar 5, 2016 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 Smith Igorbek commented on Mar 5, 2016 ContributorAuthor More actions Jason 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. dead-claudia commented on Mar 5, 2016 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. Artazor commented on Mar 5, 2016 Ability to distinguish strict tuples and open tuples looks reasonable. dead-claudia commented on Mar 5, 2016 Igor Oleinikov (@Igorbek) My opinion on that:
- Option 1: Always require
<Animal[]> <any> dogs for 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.
Artazor commented on Mar 5, 2016 @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... dead-claudia commented on Mar 5, 2016 Artazor commented on Mar 5, 2016 @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! dead-claudia commented on Mar 5, 2016 Anatoly Ressin (@Artazor)
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 I 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. KiaraGrouwstra commented on Aug 6, 2017 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 Potential solution, tie tuples to a new Tuple interface (sub-typing ReadOnlyArray), following the suggestion by Mohamed Hegazy (@mhegazy) at #16503 (comment), such as to specify known length. I'd imagine the distinct length literals 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 -> String TS'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 I've just opened a WIP PR based on the explicit length idea 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 Update: 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 mstn commented on Mar 28, 2018 @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 Marco (@mstn): I hadn't tried that -- I've no idea how your 0 workaround managed to beat the type widening issue!
That said, it seems to work also as the simplified type FixedSizeArray<N extends number, T> = { 0: any, length: N } & ReadonlyArray<T>? mstn commented on Mar 29, 2018 Yes, 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 sequence 0, 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']; // errormstn commented on Mar 29, 2018 If we think in Javascript, an array is an object with sequential numerical keys. Hence, expressions like a1 or c1 are 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
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|