Aviad Hadad
Aviad Hadad

Reputation: 1717

Return the exact same length tuple from a function

I'm trying to return from a function a tuple with the exact same length as the tuple as passed as the parameter.I thought I managed to do it through use generics, but I'm still running into error when using spread operator on the result.

What I'm trying to do is best explained through an example. I'm trying to write some helper function to validate and extract query params from an express request object.

The example first without types -

const getQueryParams = (req: Request) => (keys) => {
  const values = keys.map(key => {
    const value = req.query[key];
    if (value !== undefined && value !== null && typeof value === 'string') {
      return value;
    }
    throw new Error(`Missing query param ${key}`);
  });
  return values;
}

I added some helper types to try and say - what returns from this function is exactly the same length as it's passed:

type ArrLength<T extends readonly any[]> = T['length'];
type SameLength<T extends readonly any[]> = readonly string[] & {length: ArrLength<T>};


const getQueryParams = (req: Request) => <T extends readonly string[]>(keys: T): SameLength<T> => {
  const values = keys.map(key => {
    const value = req.query[key];
    if (value !== undefined && value !== null && typeof value === 'string') {
      return value;
    }
    throw new Error(`Missing query param ${key}`);
  }) as SameLength<T>;
  return values;
}

and it seems to work, at least partially. For example this works:

let params = getQueryParams(req)(['token', 'user', 'count'] as const);
params = ['1'] as const; //ERROR! notice typescript knows here that parmas is of length 3
params = ['1', '2', '3'] as const; //OK! notice typescript knows here that parmas is of length 3

however, trying to call a function with spread operator:

someApiMethod(...params)

I'm getting an error, Expected 3 arguments, but got 0 or more.

Full playground link

Anyone has any idea how I fix my types/do some other workaround this?

(Alternatively, if this is not solvable I would gladly accept an answer explaining why, and a relevant issue in Typescript's github repo if one exists. I can't find any)

Upvotes: 0

Views: 172

Answers (1)

jcalz
jcalz

Reputation: 329228

I think you'll find it easier if you use mapped tuple types like this:

type SameLength<T extends readonly any[]> = { [K in keyof T]: string };

This results in a genuine tuple type when you pass one in:

type TestTuple = SameLength<[false, 1, "two", Date]>;
// type TestTuple = [string, string, string, string]

Note that you might need an intermediate assertion to take the result of map() from string[] to SameLength<T>, like this:

const getQueryParams = (req: Request) => <T extends readonly string[]>(keys: T): SameLength<T> => {
  const values = keys.map(key => {
    const value = req.query[key];
    if (value !== undefined && value !== null && typeof value === 'string') {
      return value;
    }
    throw new Error(`Missing query param ${key}`);
  }) as readonly string[] as SameLength<T>; // might require intermediate assertion
  return values;
}

And now that params is a genuine tuple, you'll be able to spread it:

const handler = async (req: Request, res: any) => {
  let params = getQueryParams(req)(['token', 'user', 'count'] as const);
  const r = await someApiMethod(...params); // okay
  res.json(r);
}

Playground link to code

Upvotes: 1

Related Questions