Andy Jessop
Andy Jessop

Reputation: 287

A rest parameter must be of an array type when passing tuple type

I have a type that determines whether a type is a tuple (exact length) or an array. I want to be able to use this condition to infer the type of a parameter:

type ANumber = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;

type IsTuple<Type> = Type extends readonly unknown[] ? Type['length'] extends ANumber ? true : false : false;

function fn<T extends Record<keyof T, unknown>>() {
  return function withConfig(config: {
    [K in keyof T]: IsTuple<T[K]> extends true
      ? (...params: T[K]) => void
      : (param: T[K]) => void
  }) {};
}

fn<{ tupleType: [number, number], arrayType: number[] }>()({
  tupleType: (one, two) => {},
  arrayType: (arr) => {}
})

The actual function call works fine, one, two, and arr are all inferred correctly. However, ? (...params: T[K]) => void errors with:

A rest parameter must be of an array type

Yet...a tuple is an array type isn't it? How do I point the compiler in the right direction here?

Playground

Upvotes: 1

Views: 1708

Answers (1)

jcalz
jcalz

Reputation: 328272

If you come upon a situation where you have a generic type XXX that you know is or will be assignable to type YYY but the compiler does not (and there is an error because of it), you can often work around the situation by replacing XXX with Extract<XXX, YYY> using the Extract<T, U> utility type.

The compiler is able to understand that Extract<XXX, YYY> is assignable to both XXX and YYY. Later, when the generic type is specified, then Extract<XXX, YYY> will be exactly the same as whatever XXX resolves to, assuming you were correct about XXX being assignable to YYY in the first place.

In your example, you have T[K] and you know it is assignable to an array type like readonly unknown[] but the compiler does not know this. (It doesn't perform the sort of higher-order type analysis to see that IsTuple<T> is defined in such a way that IsTuple<T> extends true implies that T is an array type.) So you can replace T[K] with Extract<T[K], readonly unknown[]>:

function fn<T extends Record<keyof T, unknown>>() {
  return function withConfig(config: {
    [K in keyof T]: IsTuple<T[K]> extends true
    ? (...params: Extract<T[K], readonly unknown[]>) => void  // okay
    : (param: T[K]) => void
  }) { };
}

That works, and the rest of your code should work more or less the same way as it did before.

Playground link to code

Upvotes: 2

Related Questions