Typing a Proxy-based SDK whose members do not exist at run time
An SDK is a single Proxy whose get trap turns any property access into an HTTP call: reading api.users.list builds the path /users and returns a caller. The proxy target is an empty object — none of the members exist on it.
Requirements: createApi() must return a value where api.users.list() and api.orders.get(id) type-check, anything else is a compile error, and every type assertion is confined to the factory rather than spread across the call sites.
interface Api {
users: { list(): Promise<string[]> };
orders: { get(id: string): Promise<string> };
}
declare const handler: ProxyHandler<Api>;
function createApi(): Api {
// your code here
}
Write the implementation.
Declare the shape the proxy only pretends to have, then assert once in the factory: return new Proxy({} as Api, handler). A get trap returns any, so the compiler can never verify the synthesized members. One assertion keeps callers fully typed.
- ✗Expecting the compiler to derive the type from the
gettrap, which returnsany - ✗Spreading assertions across every call site instead of confining one to the factory
- ✗Believing a
gettrap fires only for keys that already exist on the proxy target
- →How would you generate the
Apiinterface from a route table with a mapped type? - →What breaks if the proxy is missing an endpoint that the interface promises?
The solution
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); // ✅ the only assertion in the whole codebase
}
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'
Why it works
The constructor is declared as new <T extends object>(target: T, handler: ProxyHandler<T>): T — the proxy takes the type of its target. The target is an empty object, so {} as Api is the only way to give it the type we want; the assertions stop there, and every other line sees a real Api.
The compiler fundamentally cannot verify the proxy: the get trap is declared to return any, and which keys it synthesizes is known only at run time. That is why the single assertion inside the factory is not a dirty hack but an honest boundary between an uncheckable runtime and fully typed callers.
⚠️ For exactly that reason the Api interface is a promise: if the proxy has no orders.get, the compiler stays silent and the call fails at run time.