3
votes

What is the reason that TS does not allow for returning empty array from generic function even if this generic parameter extends (has constrain) array? Result should derive from an array.
Below code:

// it does not work
async function handleErrors<Result extends Array<any>>(asyncCall: () => Promise<Result>): Promise<Result> {
  try {
    return await asyncCall();
  } catch (e) {
    return [];
    // return [] as Result; // also does not work - needed casting to `any`/`unknown` first
  }
}

Error:

Type 'never[]' is not assignable to type 'Result'.
'never[]' is assignable to the constraint of type 'Result', but 'Result' could be instantiated with a different subtype of constraint 'any[]'.(2322)

OR if casting applied

Conversion of type 'never[]' to type 'Result' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
'never[]' is assignable to the constraint of type 'Result', but 'Result' could be instantiated with a different subtype of constraint 'any[]'.(2352)

Type of [] is never[].

I understand that I cannot return an array with elements from this function because exact type will be inferred from the usage (which is not yet known, in function definition). It can be an array of numbers, strings, etc. so I cannot return e.g. the array of objects. But why returning empty array does not work?

Is the reason that return type can be some type which derives from the array? So just empty array will not have some properties from this type inferred from the usage?

If I use single element as generic parameter then everything works correctly:

async function handleErrors<Element>(asyncCall: () => Promise<Element[]>): Promise<Element[]> {
  try {
    return await asyncCall();
  } catch (e) {
    return [];
  }
}

Both versions with example of the usage: TS playground

1

1 Answers

4
votes

Expanding on the link in the comment, here is an example why is does not work:

async function handleErrors<Result extends Array<any>>(asyncCall: () => Promise<Result>): Promise<Result> {
  try {
    return await asyncCall();
  } catch (e) {
    return [];  // <- error, this is not allowed
  }
}

// we create a type the represents an array that can't be empty and requires one string and one number
type MyArray = [string, number];
const a: MyArray = [];                    // error
const b: MyArray = ['hello world'];       // error
const c: MyArray = ['hello world', 42];   // ah, that works

// and we can properly use that with the above method. Only that the empty array from the implementation 
// above wouldn't be compatible with our type. Therefore: the error above
handleErrors<MyArray>(() => {return new Promise((resolve, reject) => {})});

The linked answer in the comment explains the same, just with a boolean type, this example uses your implementation instead.

Edit - ah yes, but how to solve it...?

async function handleErrors<Result extends Array<any>>(asyncCall: () => Promise<Result>): Promise<Result | []> {
  try {
    return await asyncCall();
  } catch (e) {
    return [];  // Yay!
  }
}

Return a promise that is either the Result or an empty array.