Типизация SDK на Proxy, чьи члены не существуют в рантайме
SDK — это один Proxy, чей trap get превращает любое обращение к свойству в HTTP-вызов: чтение api.users.list строит путь /users и возвращает вызываемую функцию. Целевой объект прокси пуст — ни одного из членов на нём нет.
Требования: createApi() должен вернуть значение, у которого api.users.list() и api.orders.get(id) проходят проверку типов, всё остальное — ошибка компиляции, а любое приведение типа заперто внутри фабрики, а не размазано по точкам вызова.
interface Api {
users: { list(): Promise<string[]> };
orders: { get(id: string): Promise<string> };
}
declare const handler: ProxyHandler<Api>;
function createApi(): Api {
// your code here
}
Допишите реализацию.
Объявите форму, которую прокси лишь изображает, и приведите тип один раз на границе фабрики: return new Proxy({} as Api, handler). Trap get возвращает any, поэтому компилятор никогда не проверит синтезируемые члены. Одно локальное приведение — честная цена.
- ✗Ожидать, что компилятор выведет тип из trap-а
get, который возвращаетany - ✗Размазывать приведения по всем точкам вызова вместо одного внутри фабрики
- ✗Считать, что trap
getсрабатывает только для ключей, уже существующих на цели прокси
- →Как сгенерировать интерфейс
Apiиз таблицы маршрутов через mapped-тип? - →Что сломается, если у прокси нет конечной точки, которую обещает интерфейс?
Решение
interface Api {
users: { list(): Promise<string[]> };
orders: { get(id: string): Promise<string> };
}
declare const handler: ProxyHandler<Api>;
function createApi(): Api {
return new Proxy({} as Api, handler); // ✅ единственное приведение во всём коде
}
const api = createApi();
api.users.list(); // ✅ Promise<string[]>
api.orders.get('42'); // ✅ Promise<string>
api.invoices.list(); // ❌ Property 'invoices' does not exist on type 'Api'
Почему это работает
Конструктор объявлен как new <T extends object>(target: T, handler: ProxyHandler<T>): T — прокси получает тип своей цели. Целевой объект пуст, поэтому {} as Api — единственный способ дать ему нужный тип; на этом приведения заканчиваются, и весь остальной код видит настоящий Api.
Проверить прокси компилятор не может в принципе: trap get объявлен возвращающим any, а какие ключи он синтезирует — известно только в рантайме. Поэтому одно приведение внутри фабрики — не грязный хак, а честная граница между непроверяемым рантаймом и типизированными вызывающими.
⚠️ Именно из-за этого интерфейс Api — обещание: если у прокси не окажется orders.get, компилятор смолчит, а вызов упадёт в рантайме.