Прежде чем я получил ответ, я попытался воспроизвести вашу ситуацию на игровой площадке и заметил, что мне нужно изменить тип ApiRoute
на
interface ApiRoute {
query?: { [key: string]: string }; // optional
body?: any; // optional
response: any;
}
чтобы избежать ошибок. Если это не было для вас ошибкой, это потому, что вы не используете --strictNullChecks
, что вам действительно следует. Я предполагаю, что теперь мы проводим строгую проверку на ноль.
Я думаю, что ваша проблема здесь в том, что ваш ApiSpec
интерфейс говорит, что у него есть ApiRoute
свойства для каждого возможного ключа и каждого возможного подключа :
declare const myApi: MyApi;
myApi.mumbo.jumbo; // ApiRoute
myApi.bad.POST.body; // any
Этот код не является ошибкой. Итак, когда вы звоните
api.post("/bad", {
whatever: ""
});
вы просто ищете body
свойство некоторого myApi.bad.POST
, что не является ошибкой.
Так как мы можем это исправить? Я думаю, что было бы более разумно выразить тип ApiSpec
как универсальное ограничение на возможных MyApi
-подобных типах вместо конкретного типа с парой вложенных подписей индекса:
type EnsureAPIMeetsSpec<A extends object> = {
[P in keyof A]: { [M in keyof A[P]]: ApiRoute }
};
Это сопоставленный тип , который превращает A
как {foo: {bar: number, baz: string}}
в {foo: {bar: ApiRoute, baz: ApiRoute}}
. Так что если у вас есть A extends EnsureAPIMeetsSpec<A>
, то вы знаете, что A
соответствует вашим намеченным спецификациям (более или менее ... Я думаю, вы могли бы подумать, чтобы убедиться, что каждое свойство A
само по себе имеет тип object
).
И вам не нужно говорить MyApi extends ApiSpec
. Вы можете просто оставить это как
interface MyApi { /* ... */ }
и, если оно плохое, оно не будет принято ApiService
. Или, если вы хотите знать сразу, вы можете сделать это так:
interface MyApi extends EnsureAPIMeetsSpec<MyApi> { /* ... */ }
Теперь определим ApiService
. Прежде чем мы туда доберемся, давайте создадим несколько помощников типов, которые мы будем использовать в ближайшее время. Во-первых, PathsForMethod<A, M>
принимает тип API A
и имя метода M
и возвращает список строковых путей, поддерживающих этот метод:
type PathsForMethod<A extends EnsureAPIMeetsSpec<A>, M extends keyof any> = {
[P in keyof A]: M extends keyof A[P] ? (P extends string ? P : never) : never
}[keyof A];
А потом Lookup<T, K>
:
type Lookup<T, K> = K extends keyof T ? T[K] : never;
в основном T[K]
за исключением того, что компилятор не может проверить, что K
является ключом T
, он возвращает never
вместо выдачи ошибки компилятора. Это будет полезно, потому что компилятор не достаточно умен, чтобы понять, что A[PathsForMethod<A, "POST">]
имеет ключ "POST"
, хотя именно так был определен PathsForMethod
. Это морщина, которую мы должны преодолеть.
Хорошо, вот класс:
class ApiService<A extends EnsureAPIMeetsSpec<A>> {
async post<P extends PathsForMethod<A, "POST">>(
route: P,
data: Lookup<A[P], "POST">["body"]
): Promise<Lookup<A[P], "POST">["response"]> {
const resp = await fetch(route, {
method: "POST",
body: JSON.stringify(data)
});
return await resp.json();
}
}
Проходя через это ... мы ограничиваем A
до EnsureAPIMeetsSpec<A>
. Затем мы ограничиваем параметр route
только теми путями в PathsForMethod<A, "POST">
. Это автоматически исключит "/bad"
route
, которые вы пробовали в своем коде. Наконец, мы не можем просто сделать A[P]["POST"]
без ошибки компилятора, поэтому вместо этого мы делаем Lookup<A[P], "POST">
, и все работает нормально:
const api = new ApiService<MyApi>(); // accepted
const loginResponse = api.post("/login", {
username: "johnny99",
password: "rte"
});
// const loginResponse: Promise<{ token: string; }>
api.post("/bad", { // error!
whatever: ""
}); // "/bad" doesn't work
Вот так я бы начал. После этого вы можете сузить определение ApiSpec
и ApiRoute
. Например, возможно, вы хотите два типа ApiRoute
, некоторые из которых требуют body
, а другие запрещают его. И вы, вероятно, можете представить свои методы http как некоторое объединение строковых литералов, таких как "POST" | "GET" | "PUT" | "DELETE" | ...
и сужение ApiSpec
, так что методы "POST"
требуют body
, в то время как методы "GET"
запрещают это, и т. Д. Это могло бы сделать это проще чтобы компилятор гарантировал, что вы вызываете post()
только на правильных путях и что body
таких сообщений будет требоваться и определяться вместо, возможно, неопределенного.
В любом случае, надеюсь, это поможет; удачи!
Ссылка на код