Тип возвращаемого значения TypeScript зависит от свойства входного объекта

Я пытаюсь написать типобезопасную оболочку API, которая включает в себя функцию call(), которой можно передать объект endpoint, описывающий целевую конечную точку, тип тела запроса (если присутствует) и тип ожидаемого тела ответа (если присутствует).

Если указан endpoint.decoder типа Decoder<ResType>, call() должен вернуть Promise<ResType>. В противном случае call() должен вернуться Promise<void>.

Рассмотрим следующие типы:

type Encoder<MsgType extends object> = {
  encode(msg: MsgType): Uint8Array;
};

type Decoder<MsgType extends object> = {
  decode(bin: Uint8Array): MsgType;
};

type Endpoint<ReqType extends object | void = void, ResType extends object | void = void> = {
  path: string;
  decoder: ResType extends object ? Decoder<ResType> : never;
} & (
  | {
      method: 'PUT' | 'POST';
      encoder: ReqType extends object ? Encoder<ReqType> : never;
    }
  | { method: 'GET'; encoder: never }
);

Наличие свойств encoder и decoder определяет, ожидается ли тело запроса или ответа для данной конечной точки. (Тела запросов явно запрещены для запросов GET.)

Я попробовал написать функцию call() следующим образом:

async function call<ReqType extends object | void, ResType extends object | void>(
  endpoint: Endpoint<ReqType, ResType>,
  message?: ReqType
): Promise<ResType> {
    let body: BodyInit | undefined;
    if (endpoint.encoder && message) {
      body = endpoint.encoder.encode(message);  // no problem here
    }
    
    const response = await fetch('/api' + endpoint.path, { method: endpoint.method, body })

    if (endpoint.decoder) {
      const data = await response.arrayBuffer();
      /* Error: Type 'object' is not assignable to type 'ResType'.
           'object' is assignable to the constraint of type 'ResType', but 'ResType' could be instantiated with a different subtype of constraint 'void | object'. */
      return endpoint.decoder.decode(new Uint8Array(data));
    } else {
      /* Error: Type 'void' is not assignable to type 'ResType'.
           'void' is assignable to the constraint of type 'ResType', but 'ResType' could be instantiated with a different subtype of constraint 'void | object'. */
      return Promise.resolve();
    }
}

Поскольку endpoint.decoder имеет тип ResType extends object ? Decoder<ResType> : void, не должна ли проверка endpoint.decoder утверждать, что ResType расширяет object и, следовательно, не является void (и наоборот)?

Добро пожаловать в Stack Overflow. Пожалуйста, отредактируйте код, чтобы он был минимально воспроизводимым примером мы можем скопировать и вставить его в наши собственные IDE, чтобы увидеть то, что вы видите, и немедленно приступить к работе над проблемой. Если я это сделаю, меня ударят необъявленными Decoder и Encoder, а эти void и null являются несвязанными типами и не смогут легко двигаться вперед.

jcalz 26.06.2024 01:28

@jcalz Извините, я отредактировал свой вопрос, как было предложено.

60N102W 26.06.2024 03:11
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
0
2
79
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Это сложный вопрос, нам нужно посмотреть на тип параметра ReqType, чтобы понять, что не так. Сначала функция, объявленная как функция, требует ReqType в качестве типа параметра. (ReqType расширяет (объект | void)) — это тип параметра, который вызывающая функция должна передать при вызове функции, которую он требует, тип передачи должен расширяться (object | void) Поэтому ЛЮБОЙ тип, который расширяет объект, может быть выбран в качестве ReqType при вызове вашей функции, что определяется вызывающей функцией.

Затем, поскольку ваша функция заявляет, что ее тип возвращаемого значения — Promise(ResType), это фактически означает, что вы объявляете свою функцию возвращающей Promise, который содержит любой тип, который вызывающая функция выбирает в качестве ReqType.

Допустим, если вызывающая функция выберет TypeA = {aaa: string} в качестве ReqType, ваша функция примет его, потому что TypeA расширяет объект. А поскольку тип возвращаемого значения вашей функции — Promise(ReqType), это фактически означает, что ваша функция должна возвращать Promise(TypeA).

Но это не то, чего вы хотите. насколько я понимаю, вы должны объявить ReqType как ReqType расширяющий объект и тип возвращаемого значения вашей функции как Promise(ReqType | void). Таким образом, когда вызывающая сторона выбирает TypeA в качестве ReqType, ваша функция вернет Promise(TypeA | void)

Это освобождает возвращаемое значение функции от типа параметра, который передает пользователь, или аннулирует. как это

async function call<ReqType extends object, ResType extends object>(
  endpoint: Endpoint<ReqType, ResType>,
  message?: ReqType
): Promise<ResType | void> 

Я хотел избежать дополнительной проверки вызывающего объекта, чтобы определить, разрешается ли обещание в ResType или void. Оказывается, возвращаемый тип Promise<ResType | void> работает до тех пор, пока присутствуют определения перегрузки, указывающие, что и когда происходит.

60N102W 26.06.2024 05:18

о, я проверил ваш новый ответ и наконец получил то, что вы хотите. Но я хотел бы напомнить вам, что такую ​​перегруженную функцию будет довольно сложно понять. возможно, было бы лучше разделить более чем на одну функцию.

Patrick Ng 26.06.2024 11:01

Мне удалось решить проблему, используя объявления перегрузки функций, чтобы различать два случая возвращаемого типа (Promise<ResType> и Promise<void>), и ослабив ограничение extends на ResType в реализации:

async function call<ReqType extends object>(
  endpoint: Endpoint<ReqType>,
  message?: ReqType
): Promise<void>;

async function call<ReqType extends object, ResType extends object>(
  endpoint: Endpoint<ReqType, ResType>,
  message?: ReqType
): Promise<ResType>;

async function call<ReqType extends object, ResType>(
  endpoint: ResType extends object ? Endpoint<ReqType, ResType> : Endpoint<ReqType>,
  message?: ReqType
): Promise<ResType | void> {
  // implementation same as in question
}

Тип Endpoint также имел некоторые ограничения, которые требовало исправления, в первую очередь изменение extends object | void = void на extends object = never:

type Endpoint<ReqType extends object = never, ResType extends object = never> = {
  path: string;
  decoder?: Decoder<ResType>;
} & (
  | {
      method: 'PUT' | 'POST';
      encoder: ReqType extends object ? Encoder<ReqType> : never;
    }
  | { method: 'GET'; encoder?: never }
);

Другие вопросы по теме