TypeScript
Generate a typed, zero-dependency TypeScript client for your API.
Typeship generates a typed TypeScript client for your API as a focused, zero-dependency npm package. This page covers the TypeScript-specific surface; shared client behavior is on the SDK overview.
CLI and MCP packages are generated and released separately.
Package
- The npm name derives from your API's title. The Acme API produces
acme. Set a scoped or different name per project under package names, for example@acme/api. - Zero runtime dependencies. The only entries in
package.jsonare dev dependencies needed to build:typescriptand@types/node. - ESM only.
"type": "module", anexportsmap with animportentry, norequire(). Node 18 or newer, every modern bundler, browsers, and edge runtimes. - Tree-shakeable: one module per resource and
"sideEffects": false, so bundlers drop the resources you never call. - Ships as readable TypeScript source and compiles with
npm run build.main,types, andexportspoint intodist/, which holds the compiled JavaScript,.d.tsdeclarations, and declaration maps.
What is exported
import {
AcmeClient, // the client
environments, // named servers, when the spec declares two or more
VERSION, // the package version
unwrap, // ApiResult -> data, or throw the typed error
NotFoundError, // one class per documented status
UnexpectedApiError, TransportError, ValidationError,
AccountStatus, // enums as runtime values
formatDebugEvent,
} from "acme";
import type { Account, AccountsListParams, AccountsListError, ApiResult, RequestOptions } from "acme";Every schema is a type. Every operation has a <Resource><Method>Params type when it takes parameters and a <Resource><Method>Error union.
Environment variables
The client reads two, and neither is a credential:
| Variable | Effect |
|---|---|
ACME_DEBUG=1 | Turns on debug logging when debug is not set. |
ACME_WEBHOOK_KEY | The webhook signing key when webhookKey is not set. |
Credentials are passed explicitly. That keeps the client safe in browsers and edge runtimes, where process.env may not exist. The generated CLI and MCP server are the pieces that read ACME_TOKEN and ACME_BASE_URL.
Idioms
- Awaited calls return errors as data. Every awaited call returns
ApiResult<T, E>with a typed error union.unwrapopts into exceptions. See Results and errors. PagePromisefor list operations:awaitreturns one page'sApiResult;for awaitwalks every page and throws the typed error if a page fails. See Pagination.- Directional models stay honest. A component whose request and response contracts differ becomes
AccountWriteandAccountRead; identical contracts keep the cleanAccountname. - Enums are values. Request enums are closed. Response enums preserve documented autocomplete while accepting
(string & {}), so a new server value does not make a successful response impossible to represent. - Streams are
AsyncIterable<SseEvent>. See Streaming. - Webhooks verify with WebCrypto, so
unwrapisasyncand the same code runs on Node, browsers, edge runtimes, and Workers. - Async by nature. Every call returns a promise; there is no separate async client because there is nothing else to be.
Uploads
A multipart/form-data field is typed Blob. Pass a File (a Blob with a name, global in browsers and Node 20+) so the part carries a filename; a bare Blob is sent under the field name. The other fields of the body travel alongside as form parts, nested values as JSON. A raw binary body (application/octet-stream, image/png, ...) is a Blob too, sent under the spec's content type. fetch sets the multipart boundary.
await client.files.create({ file: new File([bytes], "report.pdf", { type: "application/pdf" }), purpose: "evidence" });Unions
oneOf and anyOf become TypeScript unions of the variant types, narrowed the way you narrow any union: on a discriminator property, or with in checks. Nothing is wrapped and nothing is guessed at runtime; the payload is the JSON as sent.
Response metadata
Every ApiResult, success or failure, carries response: { status, headers, requestId, rawBody }, so rate-limit headers, a Location, a request id to quote, or an unmodeled future response are one property away without going through the onResponse hook. Error classes carry the same response.
const result = await client.accounts.get("acct_123");
result.response.headers.get("ratelimit-remaining");
result.response.requestId;GraphQL
A GraphQL schema generates one method per query and mutation field on client.query and client.mutation. Arguments are the first argument and become variables. Every scalar field to depth 2 is selected by default, with a fragment per concrete type for unions and interfaces, and the optional second argument is a typed selection that replaces it and narrows the result: client.query.account({ id }, { id: true, owner: { email: true } }). Unions and interfaces take on: { TypeName: { ... } } and come back discriminated by __typename. A raw selection-set string is the escape hatch. Connection fields return a PagePromise like everything else. Errors in a 200 come back as GraphQLRequestError on the error side of the result, carrying the raw errors array. See Generate from GraphQL.
Transport
Everything rides on platform fetch. Pass fetch: to substitute your own for proxies, recording, or tests; connection reuse is whatever the runtime's fetch does (Node's undici and every browser pool keep-alive connections by default). Per-call signal composes with the per-attempt timeout, so an AbortController cancels a request, a paginated walk, or a stream. Streams are read incrementally through the same fetch.
What else is in the package
tests/*.test.mjs: one smoke test per operation onnode:testand anode:httpmock.npm testbuilds and runs them. No framework installed, nothing contacted.api.md: the complete surface reference in one file: every method, parameter table, return type, and error class.AGENTS.md: context for coding agents.- CLI and MCP files are never included in this package. They belong to their own Targets and destination repositories.
The files field publishes dist/, src/, and api.md.