-
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
Add a generic way to specify length of a tuple type #26223
Comments
You 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]; |
Oh I must have missed the trick with the 0 index. However it's not quite there, as 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 |
@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 /**
* 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 |
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:
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 |
Assume for my answers that:
Works but suffers from the neccesity for recursive definition of /** 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>>;
Yes, please!
Could be done by concatenation of a fixed-length tuple and an array (when that works): |
well, here's my scary recursive tuple generator @AlCalzone:
That's pretty creative 😄, if we had recursion-free others mentioned by @jcalz I recall hacky / legit implementations for:
|
if we can make such generetics, then we will not be able to use
|
You can already get the length of a tuple type // 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 |
The issue is not about inferring the tuple length but about specifying it |
This tricks is dangerous bugable, ts is not support tuples in core, all tricks is bullshit.
|
And one more problem
|
It is ironic for TypeScript to take so seriously for types and even template is created while length limited array types are still not supported:) |
@AlCalzone In fact, if we have |
😳 that's some next level shit. |
I'm sure you all know how to implement the type PushFront<TailT extends any[], FrontT> = (
((front : FrontT, ...rest : TailT) => any) extends ((...tuple : infer TupleT) => any) ?
TupleT :
never
);
type Tuple<ElementT, LengthT extends number, OutputT extends any[] = []> = {
0 : OutputT,
1 : Tuple<ElementT, LengthT, PushFront<OutputT, ElementT>>
}[
OutputT["length"] extends LengthT ?
0 :
1
];
//type t3 = [string, string, string]
type t3 = Tuple<string, 3>;
//type length = 0 | 3 | 1 | 2
type length = Partial<Tuple<any, 3>>['length']; With the max instantiation depth, you can't get it to be too long, though. type TupleImpl<ElementT, LengthT extends number, OutputT extends any[]> = {
0 : OutputT,
1 : TupleImpl<ElementT, LengthT, PushFront<OutputT, ElementT>>
}[
number extends OutputT["length"] ?
0 :
OutputT["length"] extends LengthT ?
0 :
1
];
type Tuple<ElementT, LengthT extends number> = (
TupleImpl<
ElementT,
LengthT,
[]
> extends infer X ?
(
X extends any[] ?
X :
never
) :
never
);
//type t41 = [string, ...39 more, string]
type t41 = Tuple<string, 41>;
/**
* Expected : type t42 = [string, ...40 more, string]
* Actual : Type instantiation is excessively deep and possibly infinite.
*/
type t42 = Tuple<string, 42>;
/**
* Expected : type t42impl = [string, ...40 more, string]
* Actual : type t42impl = [string, ...40 more, string]
*/
type t42impl = TupleImpl<string, 42, []>; |
That pesky max instantiation depth rears its head again, type PopFront<TupleT extends any[]> = (
((...tuple : TupleT) => void) extends ((head : any, ...tail : infer TailT) => void) ?
TailT :
never
);
type PushFront<TailT extends any[], FrontT> = (
((front : FrontT, ...tail : TailT) => void) extends ((...tuple : infer TupleT) => void) ?
TupleT :
never
);
/////////////////////////////////////////////////
type TupleImpl<ElementT, LengthT extends number, OutputT extends any[]> = {
0 : OutputT,
1 : TupleImpl<ElementT, LengthT, PushFront<OutputT, ElementT>>
}[
number extends OutputT["length"] ?
0 :
OutputT["length"] extends LengthT ?
0 :
1
];
type Tuple<ElementT, LengthT extends number> = (
TupleImpl<ElementT, LengthT, []> extends infer X ?
(
X extends any[] ?
X :
never
) :
never
);
//type t3 = [string, string, string]
type t3 = Tuple<string, 3>;
//type length = 0 | 3 | 1 | 2
type length = Partial<Tuple<any, 3>>['length'];
type AddOne<N extends number> = (
PushFront<Tuple<any, N>, any>["length"]
);
//type t41 = [string, ...39 more, string]
type t41 = Tuple<string, 41>;
/**
* Expected : type t42 = [string, ...40 more, string]
* Actual : Type instantiation is excessively deep and possibly infinite.
*/
type t42 = Tuple<string, 42>;
/**
* Expected : type t42impl = [string, ...40 more, string]
* Actual : type t42impl = [string, ...40 more, string]
*/
type t42impl = TupleImpl<string, 42, []>;
/**
* Expected : type t42impl = [string, ...40 more, string]
* Actual : Type instantiation is excessively deep and possibly infinite.
*/
type t43impl = TupleImpl<string, 43, []>;
//4
type _3_plus_1 = AddOne<3>;
//5
type _4_plus_1 = AddOne<4>;
//10
type _9_plus_1 = AddOne<9>;
type SubOne<N extends number> = (
PopFront<Tuple<any, N>>["length"]
);
//2
type _3_sub_1 = SubOne<3>;
//3
type _4_sub_1 = SubOne<4>;
//8
type _9_sub_1 = SubOne<9>;
/**
* Expected: -1
* Actual: 0
*
* Because tuples cannot have length -1.
*
* @todo Add signed-ness checks
*/
type _0_sub_1 = SubOne<0>;
type AddImpl<A extends number, B extends number> = {
0 : B,
1 : AddImpl<SubOne<A>, AddOne<B>>
}[
A extends 0 ?
0 :
1
];
type Add<A extends number, B extends number> = (
AddImpl<A, B> extends infer X ?
(
X extends number ?
X :
never
) :
never
);
//15
type _7_plus_8 = AddImpl<7, 8>
/**
* Excessively deep instantiation because we do not check for negative
*
* @todo Add signed-ness checks
*/
type _neg1_plus_8 = AddImpl<-1, 8>
//21
type _19_plus_2 = Add<19, 2>
/**
* Expected : 22
* Actual : Type instantiation is excessively deep and possibly infinite.
*/
type _20_plus_2 = Add<20, 2>
/**
* Expected : 22
* Actual : number
*/
type _20_plus_2impl = AddImpl<20, 2> |
🚔🚨👮♀️ UH OH, IT'S THE SELF-APPOINTED CIRCULAR CONDITIONAL TYPE POLICE 👮♂️🚨🚔 This style of construct: The only official word I've seen on this is "don't do it" and "we're not ready for this". If recursive conditional types are ever officially supported in some form, I'd expect to see them included in the baseline tests for the language, so that future releases won't break them. Right now there are no such tests. Use recursive conditional types at your own risk, and inform downstream dependencies of said risk. #26980 seems to be the canonical place to talk about this. 🚓👮♀️ THAT IS ALL 👮♂️🚓 |
@colxi Your first approach with Array length parameter: type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number
type ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : never
type Tuple<T extends any[]> =
Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>
& { [Symbol.iterator]: () => IterableIterator< ArrayItems<T> > }
type FixedLengthArray<Type, Count extends number> =
Count extends 1 ? Tuple<[Type]> :
Count extends 2 ? Tuple<[Type, Type]> :
Count extends 3 ? Tuple<[Type, Type, Type]> :
// ...
never; Tests: var myFixedLengthArray: FixedLengthArray<string, 3>
// Array declaration tests
myFixedLengthArray = [ 'a', 'b', 'c' ] // ✅ OK
myFixedLengthArray = [ 'a', 'b', 123 ] // ✅ TYPE ERROR
myFixedLengthArray = [ 'a' ] // ✅ LENGTH ERROR
myFixedLengthArray = [ 'a', 'b' ] // ✅ LENGTH ERROR
// Index assignment tests
myFixedLengthArray[1] = 'foo' // ✅ OK
myFixedLengthArray[1000] = 'foo' // ✅ INVALID INDEX ERROR
// Methods that mutate array length
myFixedLengthArray.push('foo') // ✅ MISSING METHOD ERROR
myFixedLengthArray.pop() // ✅ MISSING METHOD ERROR
// Direct length manipulation
myFixedLengthArray.length = 123 // ✅ READ-ONLY ERROR
// Destructuring
var [ a ] = myFixedLengthArray // ✅ OK
var [ a, b ] = myFixedLengthArray // ✅ OK
var [ a, b, c ] = myFixedLengthArray // ✅ OK
var [ a, b, c, d ] = myFixedLengthArray // ✅ INVALID INDEX ERROR |
What's the current status on this issue? const x: T[5] // -> [T, T, T, T, T] or, const x: Array<T, 5> // -> [T, T, T, T, T] |
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 [email protected] 🤞 |
If 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. |
This PR is friggin neat.
|
Guys, 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, type 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 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 |
@ahejlsberg thank you! TS is wild. Cool, but still wild :) |
@lazytype 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 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
(updated to use the Have not get time to try implement it using the But currently all implementations are limited by the resulting
|
Yeah, #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. |
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 Also note my multiplication/exponent/division impls were WIP and buggy. |
Also update that |
https://github.com/BenjaminAster/Better-TypeScript/blob/main/types/tuple.d.ts https://stackoverflow.com/questions/41139763/how-to-declare-a-fixed-length-array-in-typescript/60762482#60762482 https://stackoverflow.com/questions/62409401/typescript-conditionally-map-tuple-optional-elements microsoft/TypeScript#26223
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 | 4
from 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:
The text was updated successfully, but these errors were encountered: