Type a function as such it does return an array of same length

Issue

Is there a way to type a function so it says "I take an array of generic type, and will return the same type with the same length"

interface FixedLengthArray<T, L extends number> extends Array<T> {
  length: L;
}

export function shuffle<T, Arr extends Array<T>>(array: Arr):FixedLengthArray<T, Arr['length']> {
  const copy: T[] = [...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;
}

function isPairComplete<T>(arr: T[]): arr is [T, T] {
  return arr.length === 2
}

const checkThis: (pair: [number, number]) => void = ([one, two]) => {
  //
}

checkThis(shuffle([1, 2]))

Here is a link to Typescript playground

I still have a doubt on whether I’m trying to use the good thing. Maybe the solution is to reuse the same type FixedLengthArray to type my function checkThis

Solution

While you can try to describe your own fixed-length array types by providing a numeric literal type to its length property, the compiler won’t really be able to do much with it. It is probably better to just use tuple types, which already represent fixed-length array types.

If shuffle() takes a value of some array/tuple type T and produces a new array containing the same elements in some random order, then one way we can describe the return type is like this:

type Shuffled<T extends any[]> = { [I in keyof T]: T[number] };

When you make a mapped type over array/tuple types, the result is another array/tuple type of the same length. And in this instance we’re saying that each element of the resulting tuple will have the type of some element from the initial tuple. The type T[number] means "the type of the value stored at a number index of the T type", which will end up being the union of any element-specific types. So if you pass a heterogeneous tuple like [string, number] in, you will get a homogeneous tuple of the same length like [string | number, string | number] out.

Let’s test how it behaves:

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[]

Looks good. Note that for Test3, a non-tuple array like Date[] stays the same; if the compiler doesn’t know the input length, the output length is also unknown.


Now we can give your shuffle() implementation a more fitting typing:

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 works as expected here, but a few notes:

First: in order to prompt the compiler to notice shuffle([1, 2, 3]) as operating on a tuple instead of an array, I have given the array function parameter the variadic tuple type [...T] instead of just T. If you remove that, you’ll end up having to do other tricks to get the desired behavior (such as your checkThis() function to contextually require a tuple).

Second: the compiler is not clever enough to perform the sort of type analysis necessary to verify that the implementation of shuffle() actually takes an array of type [...T] and produces a result of type Shuffled<T>. Therefore I have used a type assertion to just tell the compiler that copy can be treated as Shuffled<T> when it is returned.

Playground link to code

Answered By – jcalz

Answer Checked By – Robin (AngularFixing Admin)

Leave a Reply

Your email address will not be published.