Repository navigation
Add a generic way to specify length of a tuple type #26223
Description
Activity
j-oliveras commented
on Aug 6, 2018 ContributorMore actionsYou can already do it:
interface FixedLengthArray<T extends any, L extends number> extends Array<T> { 0: T; length: L; } type Foo = FixedLengthArray<number, 5>; const foo1: Foo = [1, 2, 3, 4, 5];
Reacted by Wang Weihua, Nahla Galal, Dmitry, Amin Pourhadi, cristiano, Alexander Chashchin, Felipe S. S. Schneider, Itay Dafna, Mike Pearson and Brandon BennettReacted by Thomas Neil James Shadwell, Collie, YuChao Liang, Jose Sandoya, Richard Clayton, Manuela Zogbaum, Nahla Galal, François Levasseur, cristiano, Tomás Pietravallo and 9 moreOh I must have missed the trick with the 0 index. However it's not quite there, as
keyofdoes not work like it does with tuples:interface FixedLengthArray<T extends any, L extends number> extends Array<T> { "0": T; length: L; } type Foo = FixedLengthArray<number, 5>; type Bar = [number, number, number, number, number]; type FooKey = Exclude<keyof Foo, keyof []>; // "0" type BarKey = Exclude<keyof Bar, keyof []>; // "0" | "1" | "2" | "3" | "4"
AlCalzone arrays can be sparse, tuples cannot:
const array = new Array(5) const tuple = [1, 2, 3, 4, 5] array.length === tuple.length // true Object.keys(array) // [] Object.keys(tuple) // ["0", "1", "2", "3", "4"] let arrayCount = 0 let tupleCount = 0 for (let i in array) { arrayCount++ } for (let i in tuple) { tupleCount++ } console.log(arrayCount, tupleCount); // 0 5
Reacted by Björn Westergard- changed the title
[-]Add a way to specify length of a tuple or array type[/-][+]Add a way to specify length of a tuple type[/+]on Aug 6, 2018 - changed the title
[-]Add a way to specify length of a tuple type[/-][+]Add a generic way to specify length of a tuple type[/+]on Aug 6, 2018 James Wyatt Cready-Pyle (@jcready) I know, but I think you missed my point. I'm asking for a way to define a large tuple type for a generic length that behaves just like the ones we can write out by hand - except without writing them out by hand. See my OP:
type T1 = number[3]; // equals [number, number, number] type K1 = keyof T1; // 0 | 1 | 2
or maybe
type T1 = Tuple<number, 3>; // equals [number, number, number] type K1 = keyof T1; // 0 | 1 | 2 type T2 = Tuple<number, 100>; // equals [number, number, ..., number] (100 items total) type K2 = keyof T2; // 0 | 1 | ... | 99
in case this makes the intent clearer.
I edited the issue title to reflect this intent, as sparse arrays do actually work.
Some more context: It is currently possible to create a tuple of the type
[N, N-1, ..., 0]using this code:/** * Creates a union from the types of an Array or tuple */ type UnionOf<T extends any[]> = T[number]; /** * Returns the length of an array or tuple */ type LengthOf<T extends any[]> = T["length"]; /** * Returns all but the first item's type in a tuple/array */ export type Tail<T extends any[]> = ((...args: T) => any) extends ((head: any, ...tail: infer R) => any) ? R : never; /** * Returns the given tuple/array with the item type prepended to it */ type Unshift<List extends any[], Item> = ((first: Item, ...rest: List) => any) extends ((...list: infer R) => any) ? R : never; /** * Tests if two types are equal */ type Equals<T, S> = [T] extends [S] ? ( [S] extends [T] ? true : false ) : false; type Range<N, T extends number[] = []> = { 0: T, 1: Range<N, Unshift<T, LengthOf<T>>>, }[Equals<LengthOf<Tail<T>>, N> extends true ? 0 : 1];
For example:
type T1 = Range<3>; // [3, 2, 1, 0] type K1 = UnionOf<T1>; // 0 | 1 | 2 | 3
but this stops working for
N > 98and often freezes TypeScript while editing (#26155)Reacted by Lebedev Konstantin, Hou Chunlei, Chris, Kristóf Poduszló, Shmulik Flint, Andrey Vakhterov, Wayne Van Son, Hassan Nteifeh, Seb Insua, Christian M. Austria and 6 more- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptAwaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureThis means we'd like to hear from more people who would be helped by this feature
on Aug 6, 2018 Those of us who want full-blown dependent types for tuples (for things like type-safe immutable tuple push/pop/concat/split/etc) should give this a big 👍 .
Is there a canonical issue for dependent types for tuples? Probably needs something like one or more of:
- compile-time number arithmetic (e.g.,
Add<2,2>~4) - compile-time number comparison (e.g.,
LessThan<0,3>~true) - compile-time conversion between numeric string literals and number literals, (e.g.,
StrToNum<"0">~0) - compile-time specification of strict tuples of a given length (e.g.,
TupleLen<3, string>~[string, string, string]) ← we are here - compile-time specification of open-ended tuples of a given prefix length (e.g.,
OpenTupleLen<1, string>~[string, ...string[]]) - compile-time specification of (strict or open) tuples with optional elements starting at some index (e.g.,
TupleLenOptional<3, string, 1>~[string, string?, string?])
Most of the above can be faked up, at least for nonnegative integers less than some bound, via a bunch of hardcoded lookup types (or via the "scary" recursion of the form
type R<T> = {0: B, 1: R<F<T>>}[T extends U ? 0 : 1]), but it would be amazing if it were officially supported by the language.Reacted by AlCalzone, AnyhowStep, kiara, Jason Dreyzehner, Neonit, Abraham White, Ling Zhang, Wouter Klijn, Nurbol Alpysbayev, John Fawcett and 79 more- compile-time number arithmetic (e.g.,
Assume for my answers that:
Tuple<T, 3>is equal to[T, T, T],Concat<A, B>is equal to[...A, ...B]. Does not work yet, but can be done using two recursive types (very prone to breaking).Range<N>returns a union0 | 1 | ... | N
compile-time number arithmetic (e.g., Add<2,2> ~ 4)
LengthOf<Concat< Tuple<any, 2>, Tuple<any, 2> >>
Subtraction can probably done if mapped tuples support dropping values (#26190)compile-time number comparison (e.g., LessThan<0,3> ~ true)
Works but suffers from the neccesity for recursive definition of
Range<N>:/** Tests if N > M */ type IsGreaterThan<N, M> = N extends Exclude<Range<N>, Range<M>> ? true : false; /** Tests if N <= M */ type IsLessThanOrEqual<N, M> = Not<IsGreaterThan<N, M>>; /** Tests if N < M */ type IsLessThan<N, M> = M extends Exclude<Range<M>, Range<N>> ? true : false; /** Tests if N >= M */ type IsGreaterThanOrEqual<N, M> = Not<IsLessThan<N, M>>;
compile-time conversion between numeric string literals and number literals
Yes, please!
compile-time specification of open-ended tuples of a given prefix length
and: compile-time specification of (strict or open) tuples with optional elements starting at some indexCould be done by concatenation of a fixed-length tuple and an array (when that works):
[...fixed, ...array]or[...fixed, ...optional]KiaraGrouwstra commented
on Aug 12, 2018 ContributorMore actionswell, here's my scary recursive tuple generator AlCalzone:
Vector.
It predated features like conditional types, so shinier (and more importantly, more robust) versions may be doable now.Add<2,2> ~ 4-LengthOf<Concat<Tuple<any, 2>, Tuple<any, 2>>>That's pretty creative 😄, if we had recursion-free
Concat([...T]) andTuplethen we'd have a recursion-freeAddtoo!others mentioned by Joe Calzaretta (@jcalz) I recall hacky / legit implementations for:
LessThanStrToNumOpenTupleLen- Tuples in rest parameters and spread expressions #24897's "Optional elements in tuple types"TupleLenOptional- Tuples in rest parameters and spread expressions #24897's "Rest elements in tuple types"
if we can make such generetics, then we will not be able to use
type Vec<T extends number> = T extends 1 ? [ number ] : T extends 2 ? [ number, number ] : number[]; const sum = <TLen>(a: Vec<TLen>, b: Vec<TLen>): Vec<TLen> => a.map((v, i) => v + b[i]); const a: Vec<2> = [ 1, 2 ]; const b: Vec<2> = [ 1, 2 ]; const c = sum(a, b); // i expect c is Vec<2>... but ts think is Vec<number> // any number const testZ =c[2]; // WTF!You can already get the length of a tuple type
Tby queryingT['length'], so I wouldn't recommend trying to infer it through a conditional type. So instead of// try to infer L from Vec<L>, doesn't work 🙁 declare const sum: <L extends number>(a: Vec<L>, b: Vec<L>) => Vec<L>;
I'd do something like
// instead infer V from a vector of type V and query its length to use instead of L declare const sum: <V extends Vec<any>>(a: V, b: Vec<V['length']>) => Vec<V['length']>;
or in this particular case just
// All the lengths are the same so just use V declare const sum: <V extends Vec<any>>(a: V, b: V) => V;
Of course we don't know how an "official" implementation of
Vecor the like would behave; hopefully you'd be able to inferLfromVec<L>without any hoop-jumping.The issue is not about inferring the tuple length but about specifying it
Reacted by Flavio Vilante, Hen Greville, Rägnar O'ock and Eugene Gluhotorenko12 remaining items
- addedFix AvailableA PR has been opened for this issueA PR has been opened for this issue
on Aug 16, 2020 From PR ^
// Repeating tuples type TupleOf<T, N extends number> = N extends N ? number extends N ? T[] : _TupleOf<T, N, []> : never; type _TupleOf<T, N extends number, R extends unknown[]> = R['length'] extends N ? R : _TupleOf<T, N, [T, ...R]>; type T1 = TupleOf<string, 3>; // [string, string, string] type T2 = TupleOf<number, 0 | 2 | 4>; // [] | [number, number] | [number, number, number, number] type T3 = TupleOf<number, number>; // number[] type T4 = TupleOf<number, 100>; // Depth error
Looks nice! Will wait typescript@4.1 🤞
Reacted by Tomasz Nowak, pizzacat83, Felipe S. S. Schneider, Parbez, aaa, Chris Chudzicki, Andrii Oriekhov and Vladimir PoberezhnyIf anyone is morbidly curious for a way to achieve a greater depth limit, here's one that constructs tuples using a logarithmic approach instead of linear:
type BuildPowersOf2LengthArrays<N extends number, R extends never[][]> = R[0][N] extends never ? R : BuildPowersOf2LengthArrays<N, [[...R[0], ...R[0]], ...R]>; type ConcatLargestUntilDone<N extends number, R extends never[][], B extends never[]> = B["length"] extends N ? B : [...R[0], ...B][N] extends never ? ConcatLargestUntilDone<N, R extends [R[0], ...infer U] ? U extends never[][] ? U : never : never, B> : ConcatLargestUntilDone<N, R extends [R[0], ...infer U] ? U extends never[][] ? U : never : never, [...R[0], ...B]>; type Replace<R extends any[], T> = { [K in keyof R]: T } type TupleOf<T, N extends number> = number extends N ? T[] : { [K in N]: BuildPowersOf2LengthArrays<K, [[never]]> extends infer U ? U extends never[][] ? Replace<ConcatLargestUntilDone<K, U, []>, T> : never : never; }[N]
It has no problems with tuples with lengths of thousands. It took a pretty long while to typecheck one of length 50,000. I didn't have the patience to see how long 100K would take. Edit: It was able to eventually typecheck 100K without getting a depth error.
Reacted by Joe Calzaretta, AlCalzone, Russell Dempsey, Flavio Vilante, Picalines, Ilya Borisov, Josh Bowden, fimmind, Seb Insua, Jeff Zou and 24 moreReacted by Flavio Vilante, Simon Jacobs, andrewphillipo, David Silva, Toni Villena, Brandon Tsang, Hunter Kohler and Darryl YeoReacted by Joe Calzaretta, Gabriel Rocha de Oliveira, Russell Dempsey, Flavio Vilante, Tristan Guichaoua, Felipe S. S. Schneider, Hunter Kohler and Tyler C Laprade, CFAThis PR is friggin neat.
Here's another good use-case, typed dimensions for vectors and matrices:type Vector<Length extends number> = TupleOf<number, Length> type Matrix<Rows extends number, Columns extends number> = TupleOf<TupleOf<number, Columns>, Rows> const v: Vector<2> = [1, 2] const m: Matrix<2, 3> = [ [1, 2, 3], [1, 2, 3], ]Reacted by Joseph Kohlmann, Remi Marchand, Mike Koss, Jesper, Rägnar O'ock, Andrii Oriekhov, Brandon Bennett and MulverineGuys, could anyone help me understand this line?
type TupleOf<T, N extends number> = N extends N ? number extends N ? T[] : _TupleOf<T, N, []> : never;
Specifically,
N extends N. What could be the case whenNdoes not extendN?
I changed it to simplertype TupleOf<T, N extends number> = number extends N ? T[] : _TupleOf<T, N, []>;
and it works just as well on the provided test cases. Here is the playground link.
aigoncharov The
N extends N ? xxx : neverconditional ensures thatxxxis distributed overNwhenNis a union type. Specifically, instead of evaluatingxxxonce whenNis a union type,xxxis evaluated for each individual constituent ofNand those evaluations are then unioned together. For more on distributive conditional types, see #21316.Your modified type doesn't work for the following test case:
type T2 = TupleOf<number, 0 | 2 | 4>; // Expected [] | [number, number] | [number, number, number, number]
The result of this test case changes to
[]if you remove the distributive conditional.Reacted by Tomasz Gawel, Piotr Witek, Mike Koss, Seb Insua, Brandon Tsang, Toni Villena, Trevor Paley, 1abjora, Andrii Oriekhov and Huzefa KagdiReacted by Andrey Goncharov and Toni VillenaAnders Hejlsberg (@ahejlsberg) thank you! TS is wild. Cool, but still wild :)
lazytype
I came across the same idea but my implementation looks totally different.
Although logically it makes almost the same -> it grows array exponenentialy (but stores temporary steps in P array, then it takes items from P array as from stack and appends them to generated array, - this of course if N was not exact power of 2).
Performance is also similar, it gets slow above 2^15.type Shift<A extends Array<any>> = ((...args: A) => void) extends ((...args: [A[0], ...infer R]) => void) ? R : never; type GrowExpRev<A extends Array<any>, N extends number, P extends Array<Array<any>>> = A['length'] extends N ? A : GrowExpRev<[...A, ...P[0]][N] extends undefined ? [...A, ...P[0]] : A, N, Shift<P>>; type GrowExp<A extends Array<any>, N extends number, P extends Array<Array<any>>> = [...A, ...A][N] extends undefined ? GrowExp<[...A, ...A], N, [A, ...P]> : GrowExpRev<A, N, P>; export type Tuple<T, N extends number> = number extends N ? Array<T> : N extends 0 ? [] : N extends 1 ? [T] : GrowExp<[T], N, [[]]>;
Hello everyone, sorry for another notification, but I think that this information will be very good for newcomers and subscribers as some of the code examples above weren't working for me. There are some implementations in this thread that take the recursive approach and also the object method of setting
lengthand0to create a type for a tuple of a given type with a fixed length. However, there are limitations with both of these approaches. The recursive implementation will overload if you try to create a fixed tuple of length above 45, but for the smaller sizes it allows for nice type previews that actually look like arrays ([T, T, T, ... 16 more ... T, T]). In contrast, the object approach just showsT[] & { 0: T, length: 21 }when you hover over the type and in error messages. Thus, I have created a type (List.Locked) that can take any lengthLand any typeTto create a sized array type with items of typeTof lengthL(Scroll to the bottom for examples) using the method that would best suite the size. For sized arrays under 45 elements (inclusive) large, it uses the object approach as it does not hit the maximum type computation size that happens with the recursive approach. Here is the code:declare namespace Comparator { /** Gives back a boolean type that represents whether `T` extends `F` (similar). */ export type StrictSimilar<T, F> = T extends F ? true : false; /** Gives back a boolean type that represents whether `T` and `F` are equal. */ export type Equal<T, F> = StrictSimilar<T, F> extends true ? StrictSimilar<F, T> : false; } /** These types help you manipulate both tuples and arrays. */ declare namespace List { /** An array of `T` that can be readonly or not. */ export type LooseList<T = unknown> = T[] | readonly T[]; /** Recursive types that have tuple incrementors will overflow if they have more than 44 elements. */ export type RecTupleOverflowMax = 44; /** Gets the length of an array. */ export type Length<T extends LooseList> = T["length"]; /** Makes a tuple of length `L` with each of the elements of type `T`. */ export type Locked<L extends number, T, $Draft extends LooseList<T> = []> = // Comparator.Equal<Length<$Draft>, L> extends true ? $Draft // ship it : Comparator.Equal<Length<$Draft>, RecTupleOverflowMax> extends true // it will overflow if it's large so we need to do the hacky way ? T[] & { 0: T; length: L } : Locked<L, T, [...$Draft, T]>; /** Forms a union of all types within the array/tuple type. */ export type Squash<T extends LooseList> = T[number]; } // no error const a: List.Locked<2, true> = [true, true]; // no error const b: List.Locked<5, "hello" | "world" | 23> = [ "hello", 23, "world", "hello", 23, ]; // no error const c: List.Locked<0, true> = []; // [] // no errors const names = ["Jeff", "Joe", "James"] as const; type NonUniqueNames = List.Locked<3, List.Squash<typeof names>>; // ["Jeff" | "Joe" | "James", "Jeff" | "Joe" | "James"] const stragglers: NonUniqueNames = ["James", "Jeff", "James"]; // no overflow errors 🎉 type BigData = List.Locked<100, number>; // number[] & { 0: number; length: 100; } const d: BigData = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1];
I will include a link here when I add this code to my Deno module (type_toolkit) that has a bunch of types for everyone to use. Hopefully this code helps you out when trying to understand how this works as I tried to document it as well as I could.
I have an implementation that utilize some tricks and template literal. Performance-wise seems to be even better. I'm using it for the math operations like
AddandSubtract.Just updated theCreateTuple<L, T>based on the implementation of lazytype.(updated to use the
Digitimplementation)Have not get time to try implement it using the
Digit. :)
https://github.com/unional/type-plus/blob/main/ts/math/Add.ts
https://github.com/unional/type-plus/blob/main/ts/math/Digit.tsBut currently all implementations are limited by the resulting
Tuplesize at 10000:Type produces a tuple type that is too large to represent.
Reacted by Igor LoskutovYeah, #42448 capped tuple lengths at 10,000 which is a shame because the performance of some of these techniques was quite good even with huge tuples. Oh well.
But currently all implementations are limited by the resulting
Tuplesize at 10000:Type produces a tuple type that is too large to represent.
FWIW you should be able to still use this to scale to larger numbers. I initially created a math-in-TS solution via using only string literals before there were string-to-number conversions available.
type Foo = Int.Add<25913452093485, 3000053490953045>; // ^? 3025966943046530
However, with the
${23}` extends `${N extends infer Number}updates I converted it to use number literals. Here's a playground link for the technique: https://tsplay.dev/mb3kPW (Although the repo is a far better reference)Also note my multiplication/exponent/division impls were WIP and buggy.
Also update that
type-plusnow also uses a literal-based conversion. Supporting positive, negative, bigint, and floating points.Reacted by Carter Snook- added a commit that references this issue
on Jan 9, 2024
Search Terms
tuple, type, length
Suggestion
I'd like to see a way to specify the length of a tuple or array type when declaring it. For short tuples, it may be viable to write them out by hand like
[number, number], but whenever the length is longer (say 10+), it becomes a chore.Using the following approach to define a fixed-length tuple
has a few drawbacks:
1.: we cannot extend T because its not an interface or class.
2.: when ignoring that error, tuple literals are not inferred as tuples but as arrays when assigning them:
On the other hand, when using manually-written tuples, this works:
Use Cases
0 | 1 | 2 | 3 | 4from the keys of such a type) without having to resort to recursive types (currently broken, see TSServer hangs when using recursive types on tuples and pressing "," #26155)Examples
I'll leave the syntax up for discussion, and provide the length in the square brackets for now:
The basic rules for tuple assignability should apply, as if the types were written out by hand.
Related:
#24350
#18471
Checklist
My suggestion meets these guidelines: