The original answer was written some time ago, with typescript version 3.x. Since then the typescript version went as far as 4.94, some limitation of typescript has been lifted. Also the answer was modified due to some issues pointed in comments.
Original Answer
Actually, You can achieve this with current 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:
// OK
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'];
At that time (typescript 3.x), with this approach it was possible to construct relatively small tuples of size up to 20 items. For bigger sizes it produced "Type instantiation is excessively deep and possibly infinite". This problem was raised by @Micha Schwab in the comment below. This made to think about more efficient approach to growing arrays which resulted in the Edit 1.
EDIT 1: Bigger sizes (or "exponential growth")
This should handle bigger sizes (as basically it grows array exponentially until we get to closest 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 approach allowed to handle bigger tuple sizes (up to 2^15), although with numbers above 2^13 it was noticable slow.
This approach had also a problem with handling tuples of any
, never
and undefined
. These types satisfy the extends undefined ?
condition (the condition used to test if the index is out of generated array), and so would keep the recursion going infinitely. This problem was reported by @Victor Zhou in his comment.
EDIT 2: Tuples of never, any, or undefined
The "exponential array growth" approach cannot handle tuples of any
, never
and undefined
. This can be solved by first preparing the tuple of some "not controversial type" then rewriting the tuple with requested size to requested item type.
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]
In the meantime current typescript version become 4.94. It's time summarize and clean up the code.
EDIT 3: Typescript 4.94
The original FixedArray
type may be now written as simple as:
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 can now handle sizes up to 999.
let tuple999: FixedArray<boolean, 999>;
// let tuple999: [boolean, boolean, boolean, boolean, boolean, boolean, boolean,
// boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean,
// boolean, boolean, ... 980 more ..., boolean]
let tuple1000: FixedArray<boolean, 1000>;
// let tuple1000: any
// Error:
// Type instantiation is excessively deep and possibly infinite. ts(2589)
So we may add safe guard to return array of T if 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" approach can now handle up to 8192 (2^13) tuple size.
Above that size, it raises "Type produces a tuple type that is too large to represent. ts(2799)".
We can write it, including safe guard at size of 8192, as 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, boolean, boolean, boolean, boolean, boolean,
// boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean,
// boolean, boolean, ... 8173 more ..., boolean]
let tuple8193: FixedSizeArray<boolean, 8193>;
// let tuple8193: boolean[]