Я пытаюсь написать типобезопасную оболочку 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
(и наоборот)?
@jcalz Извините, я отредактировал свой вопрос, как было предложено.
Это сложный вопрос, нам нужно посмотреть на тип параметра 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>
работает до тех пор, пока присутствуют определения перегрузки, указывающие, что и когда происходит.
о, я проверил ваш новый ответ и наконец получил то, что вы хотите. Но я хотел бы напомнить вам, что такую перегруженную функцию будет довольно сложно понять. возможно, было бы лучше разделить более чем на одну функцию.
Мне удалось решить проблему, используя объявления перегрузки функций, чтобы различать два случая возвращаемого типа (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 }
);
Добро пожаловать в Stack Overflow. Пожалуйста, отредактируйте код, чтобы он был минимально воспроизводимым примером мы можем скопировать и вставить его в наши собственные IDE, чтобы увидеть то, что вы видите, и немедленно приступить к работе над проблемой. Если я это сделаю, меня ударят необъявленными
Decoder
иEncoder
, а этиvoid
иnull
являются несвязанными типами и не смогут легко двигаться вперед.