Обёртка над fetch, у которой тип результата выводится по эндпоинту
Сейчас вызывающие пишут const u = (await api("/users/1")) as User;, и приведение часто неверно. Реализуйте request так, чтобы тип результата следовал из одного лишь эндпоинта.
Требования: реестр маршрутов ниже — единственный источник истины. request("GET /users") обязан разрешаться в User[], request("GET /users/:id", { id: "1" }) — в User, а передача не тех params для маршрута не должна компилироваться. Никакого any и никаких приведений на местах вызова.
interface Routes {
"GET /users": { params: void; result: User[] };
"GET /users/:id": { params: { id: string }; result: User };
"POST /users": { params: { name: string }; result: User };
}
async function request<K extends keyof Routes>(
route: K,
params: Routes[K]["params"],
): Promise<Routes[K]["result"]> {
// ваш код здесь
}
Допишите реализацию.
Один реестр маршрутов. request обобщён по K extends keyof Routes, принимает params: Routes[K]['params'] и возвращает Promise<Routes[K]['result']> — оба индексные доступы по ключу эндпоинта, поэтому вывод ведёт один литерал, без приведений.
- ✗Заставлять вызывающего задавать тип результата явно вместо вывода из маршрута
- ✗Хвататься за перегрузки там, где один обобщённый параметр-ключ уже меняет оба типа
- ✗Считать, что аргумент-строковый литерал не может задать параметр типа
- →Как разрешить вызывать маршрут с
params: voidвсего одним аргументом? - →Куда добавить рантайм-проверку, чтобы
resultперестал быть непроверенным обещанием?
Решение
interface User { id: string; name: string }
interface Routes {
"GET /users": { params: void; result: User[] };
"GET /users/:id": { params: { id: string }; result: User };
"POST /users": { params: { name: string }; result: User };
}
async function request<K extends keyof Routes>(
route: K,
params: Routes[K]["params"],
): Promise<Routes[K]["result"]> {
const [method, template] = (route as string).split(" ") as [string, string];
// подстановка :id в путь; остальное уходит в query или в тело
const rest: Record<string, string> = { ...(params as object ?? {}) } as Record<string, string>;
const path = template.replace(/:(\w+)/g, (_, key: string) => {
const value = rest[key];
delete rest[key];
return encodeURIComponent(value);
});
const init: RequestInit = method === "GET"
? { method }
: { method, headers: { "content-type": "application/json" }, body: JSON.stringify(rest) };
const res = await fetch(path, init);
if (!res.ok) throw new Error(`${route}: ${res.status}`);
return (await res.json()) as Routes[K]["result"];
}
Что здесь делает вывод типов
const list = await request("GET /users", undefined); // User[]
const one = await request("GET /users/:id", { id: "1" }); // User
await request("GET /users/:id", { name: "x" });
// ~~~~~~~~~~~~~ id отсутствует — ошибка типов
K выводится из строкового литерала на месте вызова, а не из аннотации. Дальше оба индексных доступа — Routes[K]["params"] и Routes[K]["result"] — разрешаются на одном и том же K, поэтому аргумент и результат не могут разъехаться.
⚠️ Единственное приведение живёт внутри обёртки, на границе с res.json(), который по определению возвращает Promise<any>. Ни один вызывающий не пишет as — небезопасность заперта в одной строке, где и обязана быть runtime-проверка.