Home > Software engineering >  Understanding TypeScript <T extends any[]>
Understanding TypeScript <T extends any[]>

Time:11-27

getLength, it seems to work

These two functions read to me as basically the same (The second is perhaps more generic as it will accept objects with properties beyond those found on just arrays):

At runtime, they are the same javascript.

function getLength<T>(
  // v is an array of some type T
  // This could be the type 'any',
  // so I know nothing about what's inside
  v: T[] 
): number {
  return v.length;
}
function getLength2<T extends any[]>(
  // v is an array, I know nothing 
  // about what's inside
  v: T
): number {
  return v.length;
}

flattenArray... ah, okay, it doesn't really work

If I do the same substitution here, however, I start to see type errors.

function flattenArray<T>(
  a:T[][]
) : T[] {
  return a.reduce((prev:T[], curr:T[]) => [...prev,...curr], [] as T[]);
}
function flattenArray<T extends any[]>(
  a:T[]
) : T {
  return a.reduce((prev:T, curr:T) => [...prev,...curr], [] as T);
}

Errors:

  1. Type 'T[number][]' is not assignable to type 'T'
  2. Conversion of type 'never[]' to type 'T' may be a mistake

As far as I can see, I can type-cast the flattened result array and I can circumvent the type system entirely to create the empty array.

function flattenArray<T extends any[]>(
  a:T[]
) : T {
  return a.reduce((prev:T, curr:T) => [...prev,...curr] as T, [] as unknown as T);
}

This seems like a red flag to me. I suspect there's something I'm not understanding about how the type system works. When I say T extends any[], I'm thinking of T as having at least all the properties of an Array. It may have more, but not less. So I can use T as though it were an Array.

Any insight would be helpful!

CodePudding user response:

As you noted, T extends any[] means that T must be assignable to an array type, but it is allowed to have extra properties (as required by structural typing). This is not a problem for the input to the function, but cannot be guaranteed by the output. As an example:

function flattenArrayBad<T extends any[]>(a: T[]): T {
  return a.reduce<T>((prev, curr) => [...prev, ...curr], []);
  // ------------------------------> ~~~~~~~~~~~~~~~~~~
  // Type 'T[number][]' is not assignable to type 'T'.
  // T[number][]' is assignable to the constraint of type 'T', 
  // but 'T' could be instantiated with a different subtype of constraint 'any[]'
}

Here I've manually specified the type parameter in the call signature of Array.prototype.reduce to be T, so that the compiler knows to treat both the empty array initial value [] and the callback prev and return type as T. But, as you saw, it complains that T[number][] is not assignable to T. What does this mean?

Well, the compiler knows that [...prev, ...cur] is going to be a new array whose elements are the same as the array elements of T. The array elements of T are what you get when you index into an array of type T with a numeric index of type number. So T[number] is that element type. If you make an array of that, it's an Array<T[number]>, a.k.a. T[number][].

And the compiler is telling you that it cannot guarantee that [...prev, ...cur] of type T[number][] will be of type T as well. Especially in the case where T has extra properties, those properties of cur will not be copied into the new array, so [...prev, ...cur] will not have these properties. As we can verify:

const arr1 = Object.assign([1, 2, 3], { a: 1 });
const arr2 = Object.assign([4, 5, 6], { a: 2 });
const flattenedBad = flattenArrayBad([arr1, arr2]);
/* const flattenedBad: number[] & {
    a: number;
} */
try {
  console.log(flattenedBad.a.toFixed(2)); // no compiler error, but
} catch (e) {
  console.log(e); // RUNTIME            
  • Related