Home > Software engineering >  TypeScript: Constrain generic type parameter passed to a function
TypeScript: Constrain generic type parameter passed to a function

Time:09-09

I have the following generic interface (full playground):

interface Fetcher<Params,Resp> {
  fetch(params: Params): Resp;
};

It's used as a function parameter with extra constraints applied to its generic parameters:

function paginateWithSearch<Params extends (PaginationParams & SearchParams),
  Resp extends SomeItemsResp>(fetcher: Fetcher<Params, Resp>){
    //do something      
}

All constraints are represented with simple objects such as:

type SearchParams = {
    query: string
};

The tricky part is I can't make this constraints actually work in practice:

//sample implementation with SearchParams only
const searchFetcher: Fetcher<SearchParams, SomeItemsResp> = {
  fetch: function(): SomeItemsResp {
    throw new Error("Function not implemented.");
  }
}

//Should throw error but works ok - not extending PaginationParams
paginateWithSearch(searchFetcher);

The only way that I've come up with is to infer generic parameters with conditional types and pass them to the function manually:

//now throws error if inferred manually
paginateWithSearch<FetcherParamsInfer<typeof searchFetcher>, FetcherRespInfer<typeof searchFetcher>>(
  searchFetcher
);

I should be missing something as it seems like a straightforward problem.

CodePudding user response:

While it might be counter-intuitive, it works as expected.

Remember that a callback is not forced to use all provided arguments or their data:

declare function convertObjToString(obj: {
  toString: () => string;
}): void;

[new Date()].forEach(convertObjToString);
// Fine, even though it receives a Date with many more properties

Therefore it is totally fine passing a searchFetcher that takes only a SearchParams argument, while it will receive a PaginationParams & SearchParams (i.e. an object with more properties than necessary for this searchFetcher).

We could even pass a callback that takes no argument at all:

paginateWithSearch({
  fetch() { // No arg at all: okay
    return {
      items: []
    };
  }
});

Of course, that means this callback does not use any provided data, hence is bound to return unrelated response (here a fixed empty array for example).

In your sample, it would return something only related to search, disregarding pagination params.

It is up to the caller of paginateWithSearch to pass a callback that makes sense for the situation: whether it should be related to both pagination and search, only one of those, or none.

But it cannot use a callback that needs something beyond pagination and search: no sorting for example.

paginateWithSearch({
  // Error: Property 'sorting' is missing in type 'PaginationParams & SearchParams' but required in type '{ sorting: string; }'.
  fetch(params: { sorting: string }) {
    return { items: [] };
  }
})

Playground Link

  • Related