Просто хотел сделать заметку об этом, а не о чем-то другом. На днях я провел некоторое время, пытаясь понять, как я могу сделать «один тип» для всех моих клиентских функций API. После некоторой борьбы я понял, что не могу сделать так, чтобы все они extend
были более общими, поскольку… ну, это разные типы функций:
- некоторые не принимают никаких аргументов
- Некоторые принимают только идентификатор ресурса
- Некоторые принимают только тело запроса
- Некоторые принимают и идентификатор ресурса, и тело запроса.
… и все они возвращают один и тот же тип (разумеется) — что в данном случае не так уж важно.
Наконец, я решил изучить условные типы в TS. Я знал об этой возможности, но не использовал ее в «реальной» кодовой базе. Мне еще предстоит разобраться с «голыми» типами и прочим, но, в принципе, удалось получить достаточно хорошее решение (для меня):
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;
В принципе, теперь я могу создавать различные вариации типов функций API-клиентов, используя ApiClientFn
— передавая ему 3 аргумента для привязки общих типов:
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,
) => { // …
Это не то, что я изначально имел в виду. Изначально я хотел иметь один тип для представления «любой из функций клиента API». Оказалось, что на самом деле этот тип мне не нужен. Я планировал использовать его в функции useQueryApi
— в которой я хотел иметь один хук, который принимает любую из моих клиентских API функций getter и использует ее в «общем рабочем процессе», т.е. для того, чтобы DRY проблемы, такие как бросание Error
при неудачных запросах, чтобы react-query
‘s useQuery
работал правильно, и перенаправление на страницу входа, и выход из системы (и уведомление) и т.д… при определенных кодах статуса ответа.
Оказалось, что мне не нужен тип для «всех клиентских функций API, которые у меня есть», потому что этот код просто заботился о типе возврата на самом деле — так что в итоге все получилось:
export type ApiFnWrapper<TResBody> = () => ApiClientRes<TResBody>;
… и если фактическая функция клиента API принимает какие-либо args, то их всегда можно закрыть, как id
в () => getOne(id)
, ниже:
export const useSomeResource = (id: number, queryKey: string = 'queryId') => {
const { data, ...rest } = useQueryApi(() => getOne(id), {
queryKey,
});
return {
data: data?.data,
...rest,
};
};
В любом случае, ApiClientFn
«функция фабричного типа» — или как ее еще называют, позволяет мне:
- видеть все сигнатуры типов моих клиентских функций API в одном месте
- не придумывать глупые имена для различных вариаций, например,
type APIClientFnThatTakesAnId = (id: number) => ApiClientRes<TResBody>
.
… и это кажется мне победой.