A well-typed SDK layer hides untyped or loosely-typed transport details (raw JSON-RPC, fetch) behind a strongly typed API surface, so consumers never touch `any`.
ts
interface RpcRequest<Method extends string, Params extends unknown[]> {
method: Method;
params: Params;
}
type EthCall = RpcRequest<"eth_call", [{ to: string; data: string }, string]>;
type EthBlockNumber = RpcRequest<"eth_blockNumber", []>;
async function rpc<M extends string, P extends unknown[], R>(
req: RpcRequest<M, P>
): Promise<R> {
const res = await fetch("/rpc", { method: "POST", body: JSON.stringify(req) });
return (await res.json()) as R;
}
async function getBlockNumber(): Promise<bigint> {
const hex = await rpc<"eth_blockNumber", [], string>({ method: "eth_blockNumber", params: [] });
return BigInt(hex);
}Each higher-level function (`getBlockNumber`) pins down the generic parameters once, so callers get a fully-typed, method-specific function instead of a loosely typed generic `rpc` call scattered throughout the app — centralizing the one unavoidable `as R` assertion in a single well-tested place.