-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Proposal: strict and open-length tuple types #6229
Comments
Any thoughts on this? Do you think it could be proposed? |
You'd still have the problem of calling 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
|
@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, Option 1 - restrict such operations on fixed-length tuples. So let's say if array boundaries wasn't proven - give an error. |
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. |
@Igorbek My opinion on that:
|
@isiahmeadows
Agree. |
@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 |
@isiahmeadows 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 Remember, you can only do so much statically - some things are literally undetectable until runtime, no matter how powerful your type system is. |
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. |
@JsonFreeman That's one of the main reasons I want fixed-length tuples. Using tuples for variadic types won't be a problem with fixed-length tuples. Plus, it's more type safe, which is always a plus. If you're okay with open-ended tuples that subtype Arrays, in which the length can change, it's probably better to be explicit about that. (I'd rather opt out of type safety than in.) |
I agree with that, my point is that you still have to be okay with the tuple length being wrong in a case like this. |
@isiahmeadows I'd say would be better to remove length-mutating methods only, such as |
Oh, and you might want to include On Thu, Mar 10, 2016, 14:13 Isiah Meadows impinball@gmail.com wrote:
|
I meant that implicitly... Sorry about that. On Thu, Mar 10, 2016, 14:12 Igor Oleinikov notifications@github.com wrote:
|
Interesting, that these problems are expressible in the following way: |
@Artazor That seems about right AFAICT. |
The way tuples are defined in TypeScript entails that they are subtypes of arrays. This is intuitive, and it works pretty well in most cases. But it definitely has its problems, and the variadic matching is indeed one of those problems. |
Now that I think about it, array literals should be castable + assignable to Array as well as all tuple types, and this will have to be doable on the language level. Otherwise, you have a huge back-compat problem. // If either of these fail, that's a lot of existing code breakage.
let list1 = <number[]> [1, 2, 3]
let list2: number[] = [1, 2, 3] Just a thought. That's all. |
Can we also have optional tuple elements? E.g. Similar to const foobar: [ number ]|[ number, number ] = [ 1 ],
[ foo, bar = undefined ] = foobar; // currently an error Similar to const foobar: [ number, number|void ] = [ 1 ]; // currently an error |
@errorx666 Also, I'm not entirely convinced optional tuple elements are even a necessary feature. |
@isiahmeadows: Neither of those types allow |
I tried var t3b: { 0: number, 1: string } = [1, "a"];
t3b[2]; // still no error, and can't overwrite the numerical index to anything stricter than `number | string` :( Interestingly it does error if you try this member access on the type level instead. This means alternatives where the operation would be specified through a typing, such as As to making this fail: var t5: [number, string] = [1, "a", "b"]; // error (new) It seems RHS tuples can take extra properties, objects can't. If I can fix type AddPrototype<T> = Pick<T, keyof T>;
type ArrProto<T extends any[]> = AddPrototype<T> & {
[Symbol.iterator]: () => IterableIterator<T[-1]>,
[Symbol.unscopables]: () => { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }
} & { [i: number]: T[-1] };
var t1: [number, string] = <ArrProto<[1, "a"]>>[1, "a"];
// ^ ok
// v we're good if one of these errors. a/b don't, and c/d/e still break from the `TupleToObject` bug...
var t5a: [number, string] = [1, "a", "b"];
var t5b: [number, string] = <ArrProto<[1, "a", "b"]>> [1, "a", "b"];
var t5c: TupleToObject<[number, string]> = [1, "a", "b"];
var t5d: TupleToObject<[number, string]> = <TupleToObject<[1, "a", "b"]>> [1, "a", "b"];
var t5e: TupleToObject<[number, string]> = <ArrProto<[1, "a", "b"]>> [1, "a", "b"]; Still explicit conversions, type dependencies, non-DRY on the expression level, still broken, and |
well, I got export type Obj<T> = { [k: string]: T };
export type TupleHasIndex<Arr extends any[], I extends number> = ({[K in keyof Arr]: '1' } & Array<'0'>)[I];
// ^ #15768, TS2536 `X cannot be used to index Y` on generic
export type ObjectHasKey<O extends {}, K extends string> =
({[K in keyof O]: '1' } & Obj<'0'>)[K];
export type NumberToString = ['0','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24','25','26','27','28','29','30','31','32','33','34','35','36','37','38','39','40','41','42','43','44','45','46','47','48','49','50','51','52','53','54','55','56','57','58','59','60','61','62','63','64','65','66','67','68','69','70','71','72','73','74','75','76','77','78','79','80','81','82','83','84','85','86','87','88','89','90','91','92','93','94','95','96','97','98','99','100','101','102','103','104','105','106','107','108','109','110','111','112','113','114','115','116','117','118','119','120','121','122','123','124','125','126','127','128','129','130','131','132','133','134','135','136','137','138','139','140','141','142','143','144','145','146','147','148','149','150','151','152','153','154','155','156','157','158','159','160','161','162','163','164','165','166','167','168','169','170','171','172','173','174','175','176','177','178','179','180','181','182','183','184','185','186','187','188','189','190','191','192','193','194','195','196','197','198','199','200','201','202','203','204','205','206','207','208','209','210','211','212','213','214','215','216','217','218','219','220','221','222','223','224','225','226','227','228','229','230','231','232','233','234','235','236','237','238','239','240','241','242','243','244','245','246','247','248','249','250','251','252','253','254','255'];
export type Inc = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256];
export type Overwrite<K, T> = {[P in keyof T | keyof K]: { 1: T[P], 0: K[P] }[ObjectHasKey<T, P>]};
export type TupleToObject<R extends any[], I extends number = 0, Acc = {}> =
{ 1: TupleToObject<R, Inc[I], Overwrite<Acc, { [P in NumberToString[I]]: R[I] }>>, 0: Acc }[TupleHasIndex<R, I>];
const foo: [1, "a"] = [1, "a"]; // no cast with #16389
var t3: TupleToObject<typeof foo> = foo;
t3[2]; // error with `noImplicitAny`: Element implicitly has an 'any' type because type ... has no index signature. |
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. |
Potential solution, tie tuples to a new 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 |
I've just opened a WIP PR based on the explicit |
Update: got it to work. Using a flag for those concerned about breaking change, so should be win-win. |
@mstn: I hadn't tried that -- I've no idea how your |
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 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 |
We can remove the dummy generic M as suggested by tycho01 [here](microsoft/TypeScript#6229 (comment)).
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.
The text was updated successfully, but these errors were encountered: