Just wanted to make a note of this more than anything else. I spent some time the other day trying to figure out how I can make "one type" for all my API client functions. After a bit of struggling, I figured I can't really have them all extend
something more generic as… well, they're different types of functions:
- Some take no args
- Some take only the resource id
- Some take only the request body
- Some take both the resource id and the request body
… and they all return the same type (of course) - which is not really important here.
Finally, I decided to look into conditional types in TS. I've known about this feature but hadn't actually used it in a "real" code-base. I still have to get my head around "naked" types and what-not, but basically, managed to get a good enough solution (for me) with this:
export type ApiClientFn<TId, TReqBody, TResBody> = [TId, TReqBody] extends [
undefined,
undefined,
]
? () => ApiClientRes<TResBody>
: [TId, TReqBody] extends [number, undefined]
? (id: number) => ApiClientRes<TResBody>
: [TId, TReqBody] extends [undefined, TReqBody]
? (reqBody: TReqBody) => ApiClientRes<TResBody>
: [TId, TReqBody] extends [number, TReqBody]
? (id: number, reqBody: TReqBody) => ApiClientRes<TResBody>
: never;
Basically, I can now create the different variations of API client function types using ApiClientFn
- passing it 3 arguments to bind the generic types:
const getOne: ApiClientFn<number, TReqBody, TResBody> = async (id) => { // …
const getList: ApiClientFn<undefined, TReqBody, TResBody> = async () => { // …
const create: ApiClientFn<undefined, TReqBody, TResBody> = async (reqBody) => { // …
export const update: ApiClientFn<number, TReqBody, TResBody> = async (
id,
requestBody,
) => { // …
Wasn't what I initially had in mind. I initially set out to have one type to represent "any of the API client functions". Turns out I didn't actually need that type though. I had intended to use it in a useQueryApi
function - in which I wanted to have one hook that takes any of my getter client API functions and uses it in a "common workflow" i.e. in order to DRY up concerns like throwing an Error
on unsuccessful queries so react-query
's useQuery
works properly and redirecting to login page and logging out (and notification) etc… on specific status code responses.
Turns out, I didn't need a type for "all API client functions I have", because this code just cared about the return type really - so it ended up taking:
export type ApiFnWrapper<TResBody> = () => ApiClientRes<TResBody>;
… and if the actual API client function takes any args, then they can always be closed over, like id
is in () => getOne(id)
, below:
export const useSomeResource = (id: number, queryKey: string = 'queryId') => {
const { data, ...rest } = useQueryApi(() => getOne(id), {
queryKey,
});
return {
data: data?.data,
...rest,
};
};
Anyhow, the ApiClientFn
"factory type function" - or whatever it's technical term is, allows me to:
- see all type signatures of my API client functions in one place
- not have to stay coming up with silly names for the different variations e.g.
type APIClientFnThatTakesAnId = (id: number) => ApiClientRes<TResBody>
… and that seems like a win to me.
Top comments (0)