{"id":13625666,"url":"https://github.com/total-typescript/untypeable","last_synced_at":"2025-04-07T11:08:57.181Z","repository":{"id":142640736,"uuid":"612758026","full_name":"total-typescript/untypeable","owner":"total-typescript","description":"Get type-safe access to any API, with a zero-bundle size option.","archived":false,"fork":false,"pushed_at":"2023-03-16T10:06:57.000Z","size":124,"stargazers_count":373,"open_issues_count":1,"forks_count":7,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-31T10:03:40.307Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/total-typescript.png","metadata":{"files":{"readme":"readme.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2023-03-11T22:08:03.000Z","updated_at":"2025-01-17T22:44:11.000Z","dependencies_parsed_at":"2023-06-03T16:15:09.779Z","dependency_job_id":null,"html_url":"https://github.com/total-typescript/untypeable","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/total-typescript%2Funtypeable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/total-typescript%2Funtypeable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/total-typescript%2Funtypeable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/total-typescript%2Funtypeable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/total-typescript","download_url":"https://codeload.github.com/total-typescript/untypeable/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247640465,"owners_count":20971557,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T21:01:59.175Z","updated_at":"2025-04-07T11:08:57.160Z","avatar_url":"https://github.com/total-typescript.png","language":"TypeScript","readme":"# Untypeable\n\nGet type-safe access to any API, with a zero-bundle size option.\n\n## The Problem\n\nIf you're lucky enough to use [tRPC](https://trpc.io/), [GraphQL](https://graphql.org/), or [OpenAPI](https://www.openapis.org/), you'll be able to get **type-safe access to your API** - either through a type-safe RPC or codegen.\n\nBut **what about the rest of us**?\n\nWhat do you do if **your API has no types**?\n\n## Solution\n\nEnter `untypeable` - a first-class library for typing API's you don't control.\n\n- 🚀 Get **autocomplete on your entire API**, without needing to set up a single generic function.\n- 💪 **Simple to configure**, and extremely **flexible**.\n- 🤯 Choose between two modes:\n  - **Zero bundle-size**: use `import type` to ensure `untypeable` adds nothing to your bundle.\n  - **Strong types**: integrates with libraries like [Zod](https://zod.dev/) to add runtime safety to the types.\n- ✨ **Keep things organized** with helpers for merging and combining your config.\n- ❤️ You bring the fetcher, we bring the types. There's **no hidden magic**.\n\n## Quickstart\n\n`npm i untypeable`\n\n```ts\nimport { initUntypeable, createTypeLevelClient } from \"untypeable\";\n\n// Initialize untypeable\nconst u = initUntypeable();\n\ntype User = {\n  id: string;\n  name: string;\n};\n\n// Create a router\n// - Add typed inputs and outputs\nconst router = u.router({\n  \"/user\": u.input\u003c{ id: string }\u003e().output\u003cUser\u003e(),\n});\n\nconst BASE_PATH = \"http://localhost:3000\";\n\n// Create your client\n// - Pass any fetch implementation here\nconst client = createTypeLevelClient\u003ctypeof router\u003e((path, input) =\u003e {\n  return fetch(BASE_PATH + path + `?${new URLSearchParams(input)}`).then(\n    (res) =\u003e res.json(),\n  );\n});\n\n// Type-safe data access!\n// - user is typed as User\n// - { id: string } must be passed as the input\nconst user = await client(\"/user\", {\n  id: \"1\",\n});\n```\n\n## SWAPI Example\n\nWe've added a [full example](./docs/swapi-example/swapi-example.ts) of typing `swapi.dev`.\n\n## Zero-bundle mode\n\nYou can set up `untypeable` to run in zero-bundle mode. This is great for situations where you trust the API you're calling, but it just doesn't have types.\n\nTo set up zero-bundle mode, you'll need to:\n\n1. Define your router in a file called `router.ts`.\n2. Export the type of your router: `export type MyRouter = typeof router;`\n\n```ts\n// router.ts\n\nimport { initUntypeable } from \"untypeable\";\n\nconst u = initUntypeable();\n\ntype User = {\n  id: string;\n  name: string;\n};\n\nconst router = u.router({\n  \"/user\": u.input\u003c{ id: string }\u003e().output\u003cUser\u003e(),\n});\n\nexport type MyRouter = typeof router;\n```\n\n3. In a file called `client.ts`, import `createTypeLevelClient` from `untypeable/type-level-client`.\n\n```ts\n// client.ts\n\nimport { createTypeLevelClient } from \"untypeable/client\";\nimport type { MyRouter } from \"./router\";\n\nexport const client = createTypeLevelClient\u003cMyRouter\u003e(() =\u003e {\n  // your implementation...\n});\n```\n\n### How does this work?\n\nThis works because `createTypeLevelClient` is just an identity function, which directly returns the function you pass it. Most modern bundlers are smart enough to [collapse identity functions](https://github.com/evanw/esbuild/pull/1898) and erase type imports, so you end up with:\n\n```ts\n// client.ts\n\nexport const client = () =\u003e {\n  // your implementation...\n};\n```\n\n## Runtime-safe mode\n\nSometimes, you just don't trust the API you're calling. In those situations, you'll often like to _validate_ the data you get back.\n\n`untypeable` offers first-class integration with [Zod](https://zod.dev). You can pass a Zod schema to `u.input` and `u.output` to ensure that these values are validated with Zod.\n\n```ts\nimport { initUntypeable, createSafeClient } from \"untypeable\";\nimport { z } from \"zod\";\n\nconst u = initUntypeable();\n\nconst router = u.router({\n  \"/user\": u\n    .input(\n      z.object({\n        id: z.string(),\n      }),\n    )\n    .output(\n      z.object({\n        id: z.string(),\n        name: z.string(),\n      }),\n    ),\n});\n\nexport const client = createSafeClient(router, () =\u003e {\n  // Implementation...\n});\n```\n\nNow, every call made to client will have its `input` and `output` verified by the zod schemas passed.\n\n## Configuration \u0026 Arguments\n\n`untypeable` lets you be extremely flexible with the shape of your router.\n\nEach level of the router corresponds to an argument that'll be passed to your client.\n\n```ts\n// A router that looks like this:\nconst router = u.router({\n  github: {\n    \"/repos\": {\n      GET: u.output\u003cstring[]\u003e(),\n      POST: u.output\u003cstring[]\u003e(),\n    },\n  },\n});\n\nconst client = createTypeLevelClient\u003ctypeof router\u003e(() =\u003e {});\n\n// Will need to be called like this:\nclient(\"github\", \"/repos\", \"POST\");\n```\n\nYou can set up this argument structure using the methods below:\n\n### `.pushArg`\n\nUsing the `.pushArg` method when we `initUntypeable` lets us add new arguments that must be passed to our client.\n\n```ts\nimport { initUntypeable, createTypeLevelClient } from \"untypeable\";\n\n// use .pushArg to add a new argument to\n// the router definition\nconst u = initUntypeable().pushArg\u003c\"GET\" | \"POST\" | \"PUT\" | \"DELETE\"\u003e();\n\ntype User = {\n  id: string;\n  name: string;\n};\n\n// You can now optionally specify the\n// method on each route's definition\nconst router = u.router({\n  \"/user\": {\n    GET: u.input\u003c{ id: string }\u003e().output\u003cUser\u003e(),\n    POST: u.input\u003c{ name: string }\u003e().output\u003cUser\u003e(),\n    DELETE: u.input\u003c{ id: string }\u003e().output\u003cvoid\u003e(),\n  },\n});\n\n// The client now takes a new argument - method, which\n// is typed as 'GET' | 'POST' | 'PUT' | 'DELETE'\nconst client = createTypeLevelClient\u003ctypeof router\u003e((path, method, input) =\u003e {\n  let resolvedPath = path;\n  let resolvedInit: RequestInit = {};\n\n  switch (method) {\n    case \"GET\":\n      resolvedPath += `?${new URLSearchParams(input as any)}`;\n      break;\n    case \"DELETE\":\n    case \"POST\":\n    case \"PUT\":\n      resolvedInit = {\n        method,\n        body: JSON.stringify(input),\n      };\n  }\n\n  return fetch(resolvedPath, resolvedInit).then((res) =\u003e res.json());\n});\n\n// This now needs to be passed to client, and\n// is still beautifully type-safe!\nconst result = await client(\"/user\", \"POST\", {\n  name: \"Matt\",\n});\n```\n\nYou can call this as many times as you want!\n\n```ts\nconst u = initUntypeable()\n  .pushArg\u003c\"GET\" | \"POST\" | \"PUT\" | \"DELETE\"\u003e()\n  .pushArg\u003c\"foo\" | \"bar\"\u003e();\n\nconst router = u.router({\n  \"/\": {\n    GET: {\n      foo: u.output\u003cstring\u003e,\n    },\n  },\n});\n```\n\n### `.unshiftArg`\n\nYou can also add an argument at the _start_ using `.unshiftArg`. This is useful for when you want to add different base endpoints:\n\n```ts\nconst u = initUntypeable().unshiftArg\u003c\"github\", \"youtube\"\u003e();\n\nconst router = u.router({\n  github: {\n    \"/repos\": u.output\u003c{ repos: { id: string }[] }\u003e(),\n  },\n});\n```\n\n### `.args`\n\nUseful for when you want to set the args up manually:\n\n```ts\nconst u = initUntypeable().args\u003cstring, string, string\u003e();\n\nconst router = u.router({\n  \"any-string\": {\n    \"any-other-string\": {\n      \"yet-another-string\": u.output\u003cstring\u003e(),\n    },\n  },\n});\n```\n\n## Organizing your routers\n\n### `.add`\n\nYou can add more detail to a router, or split it over multiple calls, by using `router.add`.\n\n```ts\nconst router = u\n  .router({\n    \"/\": u.output\u003cstring\u003e(),\n  })\n  .add({\n    \"/user\": u.output\u003cUser\u003e(),\n  });\n```\n\n### `.merge`\n\nYou can merge two routers together using `router.merge`. This is useful for when you want to combine multiple routers (perhaps in different modules) together.\n\n```ts\nimport { userRouter } from \"./userRouter\";\nimport { postRouter } from \"./postRouter\";\n\nexport const baseRouter = userRouter.merge(postRouter);\n```\n","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftotal-typescript%2Funtypeable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftotal-typescript%2Funtypeable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftotal-typescript%2Funtypeable/lists"}