On Type Inference

theanonymousone1 pts0 comments

On Type Inference · @radekmie’s take on IT and stuffOn Type Inference<br>By Radosław Miernik · Published on 31 July 2026 · Comment on RedditTable of contents<br>IntroBackgroundWhere it all startedA follow up questionWhat about arguments?Something in betweenClosing thoughtsIntro<br>A few days ago, I started reviewing a rather simple pull request from one of my team members. I added a few comments here and there, but there was one thing that triggered me: an explicit generic type annotation was now needed in a place it wasn’t before. It was but one type, but still.We started a discussion about whether it’s a bug in the package we’re using, or if it’s something we did incorrectly… Or at least I was, because my colleague wanted to get over it. But as it’s one of the hills I will die on, we started talking.This text is more or less a summary of our discussion.Background<br>Our application has three API layers: an internal DDP API (Meteor-specific), a REST API we developed years ago, and a GraphQL API we slowly migrate all third parties to. When it comes to types, we need three different approaches. We use GraphQL Codegen, and it’s great! When it comes to DDP, we type it manually, mostly due to a lot of old and untyped code1.As for the REST API, we started gradually typing it, endpoint by endpoint. The actual implementation is rather straightforward: we have a single type that maps the endpoints to the params and response types. A slightly simplified version looks more or less like this:export type Restaurant = {<br>id: string;<br>logoUrl: string;<br>/* ... */<br>};

export type ApiEndpoints = {<br>'restaurants/v1/get': {<br>params: { id: string };<br>response: { restaurant: Restaurant };<br>};<br>'restaurants/v1/list': {<br>params: { search?: string };<br>response: { restaurants: Restaurant[] };<br>};<br>/* ... */<br>};<br>Of course, the ApiEndpoints type is, well, only a type, so we can’t really use it to fetch anything from the REST API. In the runtime, we need to execute the request somehow; either by using fetch or some popular wrapper you like. More importantly though, it must take care of authorization and the Accept-Language header. In types, it’s defined using a rather simple generic type:export type ApiFunction = Endpoint extends keyof ApiEndpoints>(<br>url: Endpoint,<br>params: ApiEndpoints[Endpoint]['params'],<br>language: string,<br>) => PromiseApiEndpoints[Endpoint]['response']>;<br>Now that we have it all set up, let’s talk about how we could use it in our React components. The api: ApiFunction is already there2, so we only need a hook around it. Ideally, it would take care of caching and other stuff, so we went with swr, as we already used it elsewhere. The hook looked like this:import useSWR from 'swr';<br>import type { ApiEndpoints, ApiFunction } from '../api';

export const useRestApi = Endpoint extends keyof ApiEndpoints>({<br>api,<br>language,<br>params,<br>url,<br>}: {<br>api: ApiFunction;<br>language: string;<br>params: ApiEndpoints[Endpoint]['params'] | null;<br>url: Endpoint;<br>}) => useSWR(params ? [url, params] : null, args => api(...args, language));<br>If you never used swr, here’s how it works: it accepts a key (here: the [url, params] tuple) that is passed to the fetcher (here: the function calling api). Caching is based on the key, and if it’s null, the query is not executed. Here’s an example of using it:// `data` is of type `{ restaurant: Restaurant } | undefined`.<br>const { data, error, isLoading } = useRestApi({<br>api,<br>language: 'en',<br>params: { id: 'example' },<br>url: 'restaurants/v1/get',<br>});<br>Where it all started<br>What was changed in the pull request? We needed to pass an additional config to some requests, i.e., we couldn’t handle it with a global configuration. Here’s the proposed change:+import type { SWRConfiguration } from 'swr';<br>export const useRestApi = ({<br>api,<br>+ config,<br>language,<br>params,<br>url,<br>}: {<br>api: ApiFunction;<br>+ config?: SWRConfiguration;<br>language: string;<br>params: ApiEndpoints[Endpoint]['params'] | null;<br>url: Endpoint;<br>-}) => useSWR(params ? [url, params] : null, args => api(...args, language));<br>+}) =><br>+ useSWR(<br>+ params ? [url, params] : null,<br>+ ([url, params]) => api(url, params, language),<br>+ config,<br>+ );<br>Can you spot the problem? I can assure you, it works as expected - config is optional, and typed correctly when passed to useRestApi. The explicit type argument passed to useSWR is noisy, but I guess we all could live with it (yes, I know I said I’d fight, but bear with me).The thing that drew my attention was the change in the fetcher function: why are the params now expanded? Was that intentional, or collateral damage a result of LLM usage? I did a quick check, and my intuition was right – it was intentional because it was “needed”… Because args was now any, and you can’t spread it. But you can destruct it in the arguments, so… A workaround.Now, what was the reason? Weren’t the inferred (implicit) types exactly the same before? Well, almost. You see, useSWR and SWRConfiguration take four and three generic types respectively, not one. There’s obviously the result (the one...

params type endpoint language apiendpoints string

Related Articles