When attempting to define fixed-length array types by assigning a numeric literal type to the length
property, the compiler may not be fully equipped to handle it efficiently. It is generally recommended to utilize tuple types, as they inherently represent fixed-length array types.
If the function shuffle()
takes an array/tuple type T
and generates a new array with elements rearranged randomly, one way to describe the return type is as follows:
type Shuffled<T extends any[]> = { [I in keyof T]: T[number] };
Creating a mapped type over array/tuple types results in another array of the same length. In this context, each element in the resulting tuple will have a type equivalent to some element from the original tuple. The notation T[number]
refers to "the type of the value stored at a number
index of type T
", which becomes a union of specific element types. Therefore, passing a heterogeneous tuple like [string, number]
will yield a homogeneous tuple of similar length such as
[string | number, string | number]
.
Let's examine its behavior through testing:
type Test1 = Shuffled<[number, number, number]>;
// type Test1 = [number, number, number]
type Test2 = Shuffled<[string, number, string]>;
// type Test2 = [string | number, string | number, string | number]
type Test3 = Shuffled<Date[]>
// type Test3 = Date[]
The tests confirm the expected outcomes. Notably, Test3
retains its structure as Date[]
; without knowledge of the input length, the output length remains unknown.
Now, we can refine the typing for your shuffle()
implementation:
function shuffle<T extends any[]>(array: [...T]) {
const copy = [...array];
for (let i = array.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy as Shuffled<T>;
}
const shuffled = shuffle([1, 2, 3]);
// const shuffled: [number, number, number]
const alsoShuffled = shuffle([1, "", false]);
// const alsoShuffled: [string | number | boolean,
// string | number | boolean, string | number | boolean]
Everything functions as intended here, though a couple of considerations emerge:
Firstly, to signify to the compiler that shuffle([1, 2, 3])
processes a tuple rather than an array, the function parameter array
is assigned the variadic tuple type [...T]
. Removing this would necessitate other methods to achieve the desired functionality (like the use of the checkThis()
function to contextually enforce a tuple).
Secondly, the compiler lacks the sophisticated type analysis needed to validate that shuffle()
actually manipulates an array
of type [...T]
and yields a result of type Shuffled<T>
. Thus, a type assertion is employed to instruct the compiler to view copy
as Shuffled<T>
when returned.
Link to code on TypeScript Playground