Home > Back-end >  Typescript return type based on property of objects passed as an array parameter
Typescript return type based on property of objects passed as an array parameter

Time:11-16

This is for TypeScript 4.3, but I can upgrade to 4.4 if need be.

I've got a function (getData in the example) that takes an array as an argument. Each member of the array is an object with a selector property set to a function with a typed return value. The array's length is arbitrary, though probably ranging from 1-10. I mention the length in case some crazy overload setup is all that will work.

The real function sources from redux if the data is initialized, or makes an api call to populate it if not. The simplified version I'm placing here just returns an array of values returned by the selectors from the array of objects passed as a parameter. The return value of this function is currently unknown[], and that's expected from the implementation. However, I had thought that TS would be able to infer the return type based on the objects passed in for specific calls to it. Something like this works for datasetA, with TS correctly inferring the return type of datasetA.selector as indicated below. I've been going through the docs and through several threads on SO, but to no avail. I'm not sure how to get TS to recognize the return types of individual calls to the function. It probably has something to do with arbitrary tuples being a weak point, or just flat out ignorance on my part. Thanks in advance if you can enlighten me!

// combined type of our redux store
type RootState = {
  a: boolean;
  b: string;
};

const state: RootState = {
  a: true,
  b: "foo",
};

type MakeDatasetConfig = <Selector extends { (a: RootState): unknown }>(
  selector: Selector
) => {
  selector: Selector;
};

const makeDataset: MakeDatasetConfig = (selector) => ({
  selector
});

const selectA = (state: RootState) => state.a;
// TS understands that datasetA.selector returns boolean;
const datasetA = makeDataset(selectA);
const selectB = (state: RootState) => state.b;
// TS understands that datasetB.selector returns string;
const datasetB = makeDataset(selectB);

type Dataset = ReturnType<MakeDatasetConfig>;

const getData = (datasets: Dataset[]) => {
  // do some other processing related to stuff omitted for simplicity
  const data = datasets.map(({ selector }) => selector(state));
  return data;
};

// Desired: [boolean]
// Currently: unknown[]
const a = getData([datasetA]);
// Desired: [boolean, string]
// Currently: unknown[]
const ab = getData([datasetA, datasetB]);

Playground

CodePudding user response:

I'm afraid you'll have to go with a crazy amount of overloads, but it seems to work:

// one param
function getData<T extends Dataset, K extends ReturnType<T["selector"]>>(datasets: [T]): [K];

// two params
function getData<
    T1 extends Dataset,
    R1 extends ReturnType<T1["selector"]>,
    T2 extends Dataset,
    R2 extends ReturnType<T2["selector"]>
>(datasets: [T1, T2]): [R1, R2];

function getData(datasets: Dataset[]): any {
    // do some other processing related to stuff omitted for simplicity
    const data = datasets.map(({ selector }) => selector(state));
    return data;
};

// [boolean]
const a = getData([datasetA]);
// [boolean, string]
const ab = getData([datasetA, datasetB]);

For 3 array elements you'd have to create another overload with 6 generic type variables, for 4 - 8, and so on.

  • Related