← Back to archive

Repo of the Day

total-typescript/untypeable

Published: Mar 14, 2023

Open repository ↗

Get type-safe access to any API, with a zero-bundle size option.

Summary

Untypeable is a TypeScript library that gives you autocomplete and type-checking for HTTP APIs that don't ship their own types. It works by letting you describe your endpoints with a router, then generates a typed client that infers inputs and outputs at the call site. You choose between a zero-bundle-size mode (type-only) or a runtime-safe mode that integrates with Zod for validation.

What it is useful for

Untypeable fits when you depend on a third-party or internal HTTP service that doesn't expose TypeScript types, a GraphQL schema, or an OpenAPI spec you want to use. Instead of hand-typing fetch calls and casting responses to any, you describe each endpoint once and get inference throughout the codebase.

Two situations the README highlights:

  • You trust the API but it just has no types — use zero-bundle mode for compile-time safety with no shipped JavaScript.
  • You don't trust the API — use runtime-safe mode with Zod so every input and output is validated on each call.

How engineers can use it

Install with npm i untypeable. The documented quickstart has three steps.

  1. Initialize the library and define a router with input and output types per endpoint:
const u = initUntypeable();
const router = u.router({
  "/user": u.input<{ id: string }>().output<User>(),
});
  1. Create a client by passing your own fetcher:
const client = createTypeLevelClient<typeof router>((path, input) => {
  return fetch(BASE_PATH + path + `?${new URLSearchParams(input)}`).then(
    (res) => res.json(),
  );
});
  1. Call the client with full inference:
const user = await client("/user", { id: "1" });

For zero bundle impact, the README documents putting the router in router.ts and exporting export type MyRouter = typeof router, then importing the client from untypeable/client with import type in client.ts. The library relies on the bundler collapsing the identity function createTypeLevelClient so it erases itself from the output.

For runtime validation, swap createTypeLevelClient for createSafeClient(router, ...) and pass Zod schemas to u.input(...) and u.output(...). The router shape can be customized with .pushArg<...>(), .unshiftArg<...>(), and .args<...>() to control how arguments are passed to the client, and routers can be split across files with .add(...) and .merge(...). A full working example against swapi.dev is included in docs/swapi-example/.

Documented limitations: you supply the fetcher, so error handling, retries, and headers are your responsibility. Zero-bundle mode depends on your bundler collapsing identity functions and erasing type imports. The runtime-safe path requires Zod as an additional dependency.