Создание различных сигнатур типов функций Typescript на основе типов, привязанных к generics при использовании

Просто хотел сделать заметку об этом, а не о чем-то другом. На днях я провел некоторое время, пытаясь понять, как я могу сделать «один тип» для всех моих клиентских функций 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>.

… и это кажется мне победой.

Оцените статью
Procodings.ru
Добавить комментарий