The original response was crafted some time back, using typescript version 3.x. Since then, typescript has advanced to version 4.94, lifting certain limitations that existed. Additionally, modifications were made to address issues raised in the comments.
Initial Response
It is possible to achieve this with the current version of typescript:
type Grow<T, A extends Array<T>> =
((x: T, ...xs: A) => void) extends ((...a: infer X) => void) ? X : never;
type GrowToSize<T, A extends Array<T>, N extends number> =
{ 0: A, 1: GrowToSize<T, Grow<T, A>, N> }[A['length'] extends N ? 0 : 1];
export type FixedArray<T, N extends number> = GrowToSize<T, [], N>;
Examples:
// Success
const fixedArr3: FixedArray<string, 3> = ['a', 'b', 'c'];
// Error:
// Type '[string, string, string]' is not assignable to type '[string, string]'.
// Types of property 'length' are incompatible.
// Type '3' is not assignable to type '2'.ts(2322)
const fixedArr2: FixedArray<string, 2> = ['a', 'b', 'c'];
// Error:
// Property '3' is missing in type '[string, string, string]' but required in type
// '[string, string, string, string]'.ts(2741)
const fixedArr4: FixedArray<string, 4> = ['a', 'b', 'c'];
During typescript 3.x era, this approach allowed for creating tuples of small sizes up to 20 elements. However, attempting larger sizes resulted in a "Type instantiation is excessively deep and possibly infinite" error, as highlighted by @Micha Schwab in the comment below. This led to exploring a more efficient array growth method, leading to Edit 1.
EDIT 1: Handling Larger Sizes
This revised method can handle larger tuple sizes through exponential array growth until reaching the nearest power of two:
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 : {
0: GrowExpRev<[...A, ...P[0]], N, P>,
1: GrowExpRev<A, N, Shift<P>>
}[[...A, ...P[0]][N] extends undefined ? 0 : 1];
type GrowExp<A extends Array<any>, N extends number, P extends Array<Array<any>>> = A['length'] extends N ? A : {
0: GrowExp<[...A, ...A], N, [A, ...P]>,
1: GrowExpRev<A, N, P>
}[[...A, ...A][N] extends undefined ? 0 : 1];
export type FixedSizeArray<T, N extends number> = N extends 0 ? [] : N extends 1 ? [T] : GrowExp<[T, T], N, [[T]]>;
This enhancement enabled handling tuple sizes up to 2^15, though performance started degrading noticeably beyond 2^13. Furthermore, it struggled with `any`, `never`, and `undefined` types, triggering an infinite recursion loop due to them satisfying the `extends undefined ?` condition which checked for exceeding array index, as identified by @Victor Zhou's comment.
EDIT 2: Addressing Specific Tuple Types
The "exponential array growth" technique had difficulties with tuples containing `any`, `never`, or `undefined`. One workaround involved preparing a tuple with a generic type before transforming it to the desired item type based on the requested size.
type MapItemType<T, I> = { [K in keyof T]: I };
export type FixedSizeArray<T, N extends number> =
N extends 0 ? [] : MapItemType<GrowExp<[0], N, []>, T>;
Examples:
var tupleOfAny: FixedSizeArray<any, 3>; // [any, any, any]
var tupleOfNever: FixedSizeArray<never, 3>; // [never, never, never]
var tupleOfUndef: FixedSizeArray<undefined, 2>; // [undefined, undefined]
With the current typescript version now at 4.94, it's time to summarize and refine the code.
EDIT 3: Introducing Typescript 4.94 Updates
The initial FixedArray
type can now be simplified as follows:
type GrowToSize<T, N extends number, A extends T[]> =
A['length'] extends N ? A : GrowToSize<T, N, [...A, T]>;
export type FixedArray<T, N extends number> = GrowToSize<T, N, []>;
This version can accommodate sizes up to 999.
let tuple999: FixedArray<boolean, 999>;
// let tuple999: [boolean, boolean, ... (repeated 980 times), boolean]
let tuple1000: FixedArray<boolean, 1000>;
// let tuple1000: any
// Error:
// Type instantiation is excessively deep and possibly infinite. ts(2589)
To ensure safety, we add a safeguard to return an array of type T if the tuple size exceeds 999.
type GrowToSize<T, N extends number, A extends T[], L extends number = A['length']> =
L extends N ? A : L extends 999 ? T[] : GrowToSize<T, N, [...A, T]>;
export type FixedArray<T, N extends number> = GrowToSize<T, N, []>;
let tuple3: FixedArray<boolean, 3>; // [boolean, boolean, boolean]
let tuple1000: FixedArray<boolean, 1000>; // boolean[]
The "exponential array growth" strategy can now cater to tuple sizes up to 8192 (2^13).
Beyond that size, it triggers a "Type produces a tuple type that is too large to represent. ts(2799)" error.
The updated version, incorporating a safeguard at size 8192, is presented below:
type Shift<A extends Array<any>> =
((...args: A) => void) extends ((...args: [A[0], ...infer R]) => void) ? R : never;
type GrowExpRev<A extends any[], N extends number, P extends any[][]> =
A['length'] extends N ? A : [...A, ...P[0]][N] extends undefined ? GrowExpRev<[...A, ...P[0]], N, P> : GrowExpRev<A, N, Shift<P>>;
type GrowExp<A extends any[], N extends number, P extends any[][], L extends number = A['length']> =
L extends N ? A : L extends 8192 ? any[] : [...A, ...A][N] extends undefined ? GrowExp<[...A, ...A], N, [A, ...P]> : GrowExpRev<A, N, P>;
type MapItemType<T, I> = { [K in keyof T]: I };
export type FixedSizeArray<T, N extends number> =
N extends 0 ? [] : MapItemType<GrowExp<[0], N, []>, T>;
let tuple8192: FixedSizeArray<boolean, 8192>;
// let tuple8192: [boolean, boolean, ... (repeated 8173 times), boolean]
let tuple8193: FixedSizeArray<boolean, 8193>;
// let tuple8193: boolean[]