Repository: total-typescript/untypeable
Branch: main
Commit: 95e069702995
Files: 28
Total size: 42.4 KB
Directory structure:
gitextract_qcn3yl_o/
├── .changeset/
│ ├── README.md
│ └── config.json
├── .github/
│ └── workflows/
│ ├── main.yml
│ └── publish.yml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── docs/
│ └── swapi-example/
│ ├── swapi-example.ts
│ └── types.ts
├── package.json
├── readme.md
├── src/
│ ├── client.ts
│ ├── constants.ts
│ ├── index.ts
│ ├── playground.ts
│ ├── safe-client.ts
│ ├── tests/
│ │ ├── add.test.ts
│ │ ├── dynamic-args.test.ts
│ │ ├── merge.test.ts
│ │ ├── repl.test.ts
│ │ ├── router-definition.test.ts
│ │ ├── type-only.test.ts
│ │ ├── utils.ts
│ │ └── with-schemas.test.ts
│ ├── types.ts
│ └── untypeable.ts
├── tsconfig.json
└── tsup.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .changeset/README.md
================================================
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
================================================
FILE: .changeset/config.json
================================================
{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
================================================
FILE: .github/workflows/main.yml
================================================
name: CI
on:
push:
branches:
- "**"
pull_request:
branches:
- "**"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 7
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run ci
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish
on:
push:
branches:
- "main"
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 7
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@v1
with:
publish: pnpm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .gitignore
================================================
node_modules
dist
================================================
FILE: .npmignore
================================================
src
node_modules
pnpm-lock.yaml
tsconfig.json
================================================
FILE: CHANGELOG.md
================================================
# untypeable
## 0.2.0
### Minor Changes
- 9db7f9b: Made it so you don't need to pass an empty object to the client if the input is entirely partial.
## 0.1.0
### Minor Changes
- 0441952: Initial release of untypeable - check the readme for details!
================================================
FILE: docs/swapi-example/swapi-example.ts
================================================
import { createTypeLevelClient, initUntypeable } from "../../src/index";
import { Person, Paginated, Planet, Film, Vehicle } from "./types";
const u = initUntypeable();
/**
* Create the router
*/
const router = u.router({
"/people/:id": u.input<{ id: string }>().output<Person>(),
"/people": u.output<Paginated<Person>>(),
"/planets/:id": u.input<{ id: string }>().output<Planet>(),
"/planets": u.output<Paginated<Planet>>(),
"/films/:id": u.input<{ id: string }>().output<Film>(),
"/films": u.output<Paginated<Film>>(),
"/vehicles/:id": u.input<{ id: string }>().output<Vehicle>(),
"/vehicles": u.output<Paginated<Vehicle>>(),
});
/**
* Create the client, using the zero-bundle method
*/
export const fetchFromSwapi = createTypeLevelClient<typeof router>(
(path, input = {}) => {
// Replace dynamic path params in url
const pathWithParams = path.replace(
/:([a-zA-Z0-9_]+)/g,
(_, key) => input[key],
);
return fetch(`https://swapi.dev/api${pathWithParams}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
}).then((res) => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.json();
});
},
);
================================================
FILE: docs/swapi-example/types.ts
================================================
export type Person = {
name: string;
height: string;
mass: string;
hair_color: string;
skin_color: string;
birth_year: string;
gender: string;
homeworld: string;
films: string[];
};
export type Planet = {
climate: string;
diameter: string;
gravity: string;
name: string;
orbital_period: string;
population: string;
residents: string[];
};
export type Film = {
title: string;
episode_id: number;
opening_crawl: string;
director: string;
producer: string;
release_date: string;
characters: string[];
planets: string[];
starships: string[];
vehicles: string[];
};
export type Vehicle = {
name: string;
model: string;
manufacturer: string;
cost_in_credits: string;
length: string;
max_atmosphering_speed: string;
};
export type Paginated<T> = {
count: number;
next: string | null;
previous: string | null;
results: T[];
};
================================================
FILE: package.json
================================================
{
"name": "untypeable",
"version": "0.2.1",
"description": "Get type-safe access to any API, with a zero-bundle size option.",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./client": {
"types": "./dist/client.d.ts",
"import": "./dist/client.mjs",
"require": "./dist/client.js"
},
"./package.json": "./package.json"
},
"scripts": {
"build": "tsup",
"lint": "tsc",
"ci": "npm run build && npm run lint && npm run test",
"prepublish": "npm run ci",
"test": "vitest run",
"dev": "vitest",
"release": "npm run ci && changeset publish"
},
"keywords": [],
"author": "Matt Pocock",
"license": "ISC",
"devDependencies": {
"@changesets/cli": "^2.26.0",
"tsup": "^6.6.3",
"typescript": "^4.9.5",
"vite": "^4.1.4",
"vitest": "^0.29.2",
"zod": "^3.21.4"
}
}
================================================
FILE: readme.md
================================================
# Untypeable
Get type-safe access to any API, with a zero-bundle size option.
## The Problem
If 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.
But **what about the rest of us**?
What do you do if **your API has no types**?
## Solution
Enter `untypeable` - a first-class library for typing API's you don't control.
- 🚀 Get **autocomplete on your entire API**, without needing to set up a single generic function.
- 💪 **Simple to configure**, and extremely **flexible**.
- 🤯 Choose between two modes:
- **Zero bundle-size**: use `import type` to ensure `untypeable` adds nothing to your bundle.
- **Strong types**: integrates with libraries like [Zod](https://zod.dev/) to add runtime safety to the types.
- ✨ **Keep things organized** with helpers for merging and combining your config.
- ❤️ You bring the fetcher, we bring the types. There's **no hidden magic**.
## Quickstart
`npm i untypeable`
```ts
import { initUntypeable, createTypeLevelClient } from "untypeable";
// Initialize untypeable
const u = initUntypeable();
type User = {
id: string;
name: string;
};
// Create a router
// - Add typed inputs and outputs
const router = u.router({
"/user": u.input<{ id: string }>().output<User>(),
});
const BASE_PATH = "http://localhost:3000";
// Create your client
// - Pass any fetch implementation here
const client = createTypeLevelClient<typeof router>((path, input) => {
return fetch(BASE_PATH + path + `?${new URLSearchParams(input)}`).then(
(res) => res.json(),
);
});
// Type-safe data access!
// - user is typed as User
// - { id: string } must be passed as the input
const user = await client("/user", {
id: "1",
});
```
## SWAPI Example
We've added a [full example](./docs/swapi-example/swapi-example.ts) of typing `swapi.dev`.
## Zero-bundle mode
You 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.
To set up zero-bundle mode, you'll need to:
1. Define your router in a file called `router.ts`.
2. Export the type of your router: `export type MyRouter = typeof router;`
```ts
// router.ts
import { initUntypeable } from "untypeable";
const u = initUntypeable();
type User = {
id: string;
name: string;
};
const router = u.router({
"/user": u.input<{ id: string }>().output<User>(),
});
export type MyRouter = typeof router;
```
3. In a file called `client.ts`, import `createTypeLevelClient` from `untypeable/type-level-client`.
```ts
// client.ts
import { createTypeLevelClient } from "untypeable/client";
import type { MyRouter } from "./router";
export const client = createTypeLevelClient<MyRouter>(() => {
// your implementation...
});
```
### How does this work?
This 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:
```ts
// client.ts
export const client = () => {
// your implementation...
};
```
## Runtime-safe mode
Sometimes, you just don't trust the API you're calling. In those situations, you'll often like to _validate_ the data you get back.
`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.
```ts
import { initUntypeable, createSafeClient } from "untypeable";
import { z } from "zod";
const u = initUntypeable();
const router = u.router({
"/user": u
.input(
z.object({
id: z.string(),
}),
)
.output(
z.object({
id: z.string(),
name: z.string(),
}),
),
});
export const client = createSafeClient(router, () => {
// Implementation...
});
```
Now, every call made to client will have its `input` and `output` verified by the zod schemas passed.
## Configuration & Arguments
`untypeable` lets you be extremely flexible with the shape of your router.
Each level of the router corresponds to an argument that'll be passed to your client.
```ts
// A router that looks like this:
const router = u.router({
github: {
"/repos": {
GET: u.output<string[]>(),
POST: u.output<string[]>(),
},
},
});
const client = createTypeLevelClient<typeof router>(() => {});
// Will need to be called like this:
client("github", "/repos", "POST");
```
You can set up this argument structure using the methods below:
### `.pushArg`
Using the `.pushArg` method when we `initUntypeable` lets us add new arguments that must be passed to our client.
```ts
import { initUntypeable, createTypeLevelClient } from "untypeable";
// use .pushArg to add a new argument to
// the router definition
const u = initUntypeable().pushArg<"GET" | "POST" | "PUT" | "DELETE">();
type User = {
id: string;
name: string;
};
// You can now optionally specify the
// method on each route's definition
const router = u.router({
"/user": {
GET: u.input<{ id: string }>().output<User>(),
POST: u.input<{ name: string }>().output<User>(),
DELETE: u.input<{ id: string }>().output<void>(),
},
});
// The client now takes a new argument - method, which
// is typed as 'GET' | 'POST' | 'PUT' | 'DELETE'
const client = createTypeLevelClient<typeof router>((path, method, input) => {
let resolvedPath = path;
let resolvedInit: RequestInit = {};
switch (method) {
case "GET":
resolvedPath += `?${new URLSearchParams(input as any)}`;
break;
case "DELETE":
case "POST":
case "PUT":
resolvedInit = {
method,
body: JSON.stringify(input),
};
}
return fetch(resolvedPath, resolvedInit).then((res) => res.json());
});
// This now needs to be passed to client, and
// is still beautifully type-safe!
const result = await client("/user", "POST", {
name: "Matt",
});
```
You can call this as many times as you want!
```ts
const u = initUntypeable()
.pushArg<"GET" | "POST" | "PUT" | "DELETE">()
.pushArg<"foo" | "bar">();
const router = u.router({
"/": {
GET: {
foo: u.output<string>,
},
},
});
```
### `.unshiftArg`
You can also add an argument at the _start_ using `.unshiftArg`. This is useful for when you want to add different base endpoints:
```ts
const u = initUntypeable().unshiftArg<"github", "youtube">();
const router = u.router({
github: {
"/repos": u.output<{ repos: { id: string }[] }>(),
},
});
```
### `.args`
Useful for when you want to set the args up manually:
```ts
const u = initUntypeable().args<string, string, string>();
const router = u.router({
"any-string": {
"any-other-string": {
"yet-another-string": u.output<string>(),
},
},
});
```
## Organizing your routers
### `.add`
You can add more detail to a router, or split it over multiple calls, by using `router.add`.
```ts
const router = u
.router({
"/": u.output<string>(),
})
.add({
"/user": u.output<User>(),
});
```
### `.merge`
You can merge two routers together using `router.merge`. This is useful for when you want to combine multiple routers (perhaps in different modules) together.
```ts
import { userRouter } from "./userRouter";
import { postRouter } from "./postRouter";
export const baseRouter = userRouter.merge(postRouter);
```
================================================
FILE: src/client.ts
================================================
import {
ArgsFromRouter,
LooseUntypeableHandler,
RoutesFromRouter,
UntypeableHandler,
UntypeableRouter,
} from "./types";
/**
* Creates an API client that does not check the input or output
* at runtime. For runtime checking, use createSafeClient.
*
* If you're using createTypeLevelClient, we recommend importing
* it from 'untypeable/client' to minimize bundle size.
*
* @example
*
* const client = createTypeLevelClient<MyRouterType>((path) => {
* return fetch(path).then(res => res.json());
* });
*/
export const createTypeLevelClient = <
TRouter extends UntypeableRouter<any, any>,
TArgs extends readonly string[] = ArgsFromRouter<TRouter>,
TRoutes extends Record<string, any> = RoutesFromRouter<TRouter>,
>(
handler: LooseUntypeableHandler<TArgs>,
): UntypeableHandler<TArgs, TRoutes> => {
return handler as any;
};
================================================
FILE: src/constants.ts
================================================
export const SEPARATOR = "__";
================================================
FILE: src/index.ts
================================================
export * from "./client";
export * from "./types";
export * from "./untypeable";
export * from "./safe-client";
================================================
FILE: src/playground.ts
================================================
import { initUntypeable, createTypeLevelClient } from "untypeable";
// Initialize untypeable
const u = initUntypeable();
type User = {
id: string;
name: string;
};
// Create a router
// - Add typed inputs and outputs
const router = u.router({
"/user": u.input<{ id: string }>().output<User>(),
});
// Create your client
// - Pass any fetch implementation here
const client = createTypeLevelClient<typeof router>((path, input) => {
return fetch(path + `?${new URLSearchParams(input)}`).then((res) =>
res.json(),
);
});
// Type-safe data access!
// - user is typed as User
// - { id: string } must be passed as the input
const user = client("/user", {
id: "1",
});
================================================
FILE: src/safe-client.ts
================================================
import { SEPARATOR } from "./constants";
import {
AcceptedParser,
ArgsFromRouter,
LooseUntypeableHandler,
RoutesFromRouter,
Schemas,
UntypeableHandler,
UntypeableRouter,
} from "./types";
export const createSafeClient = <
TRouter extends UntypeableRouter<any, any>,
TArgs extends ArgsFromRouter<TRouter>,
TRoutes extends RoutesFromRouter<TRouter>,
>(
router: TRouter,
handler: LooseUntypeableHandler<TArgs>,
): UntypeableHandler<TArgs, TRoutes> => {
return (async (...args: any[]) => {
let schemas: Schemas;
const argsExceptLast = args.slice(0, -1);
const matchingSchemas = router._schemaMap.get(
argsExceptLast.join(SEPARATOR),
);
const secondSchemaMatchAttempt = router._schemaMap.get(
args.join(SEPARATOR),
);
let shouldCheckInput: boolean;
if (matchingSchemas) {
schemas = matchingSchemas;
shouldCheckInput = true;
} else if (secondSchemaMatchAttempt) {
schemas = secondSchemaMatchAttempt;
shouldCheckInput = false;
} else {
throw new Error(
`No matching schema found for args: ${argsExceptLast.join(", ")}`,
);
}
const inputSchema = resolveParser(schemas.input);
const outputSchema = resolveParser(schemas.output);
let output;
if (shouldCheckInput) {
const input = args[args.length - 1];
const parsedInput = inputSchema(input);
output = await (handler as any)(...argsExceptLast, parsedInput);
} else {
output = await (handler as any)(...args);
}
return outputSchema(output);
}) as any;
};
const resolveParser = <T>(parser: AcceptedParser<T> | undefined) => {
if (typeof parser === "function") {
return parser;
} else if (typeof parser === "undefined") {
return (x: any) => x;
} else {
return parser.parse;
}
};
================================================
FILE: src/tests/add.test.ts
================================================
import { expect, it, vitest } from "vitest";
import { z } from "zod";
import { createSafeClient } from "../safe-client";
import { initUntypeable } from "../untypeable";
const u = initUntypeable();
const router = u
.router({
inputNeeded: u.input(z.string()).output(z.object({ hello: z.string() })),
})
.add({
inputNotNeeded: u.output(z.object({ hello: z.string() })),
});
it('Should pick up schemas added by the "add" method', async () => {
const fn = vitest.fn();
const client = createSafeClient(router, fn);
fn.mockResolvedValueOnce({ hello: "world" });
await client("inputNeeded", "hello");
expect(fn).toHaveBeenLastCalledWith("inputNeeded", "hello");
fn.mockResolvedValueOnce({ hello: "world" });
await client("inputNotNeeded");
});
================================================
FILE: src/tests/dynamic-args.test.ts
================================================
import { it, vitest } from "vitest";
import { createTypeLevelClient } from "../client";
import { initUntypeable } from "../untypeable";
it("Should let you specify 2 args", async () => {
const u = initUntypeable().args<"GET" | "POST", "SOMETHING" | "ELSE">();
const router = u.router({
GET: {
ELSE: u.output<string>(),
},
});
const client = createTypeLevelClient<typeof router>(vitest.fn());
await client("GET", "ELSE");
// @ts-expect-error
await client("GET", "SOMETHING");
// @ts-expect-error
await client("POST", "ELSE");
});
it("Should default to only requiring one arg", async () => {
const u = initUntypeable();
const router = u.router({
GET: u.output<string>(),
});
const client = createTypeLevelClient<typeof router>(vitest.fn());
await client("GET");
// @ts-expect-error
await client("POST");
});
================================================
FILE: src/tests/merge.test.ts
================================================
import { expect, it, vitest } from "vitest";
import { z } from "zod";
import { createSafeClient } from "../safe-client";
import { initUntypeable } from "../untypeable";
const u = initUntypeable();
const router1 = u.router({
inputNeeded: u.input(z.string()).output(z.object({ hello: z.string() })),
});
const router2 = u.router({
inputNotNeeded: u.output(z.object({ hello: z.string() })),
});
const router = router2.merge(router1);
it('Should pick up schemas added by the "merge" method', async () => {
const fn = vitest.fn();
const client = createSafeClient(router, fn);
fn.mockResolvedValueOnce({ hello: "world" });
await client("inputNeeded", "hello");
expect(fn).toHaveBeenLastCalledWith("inputNeeded", "hello");
fn.mockResolvedValueOnce({ hello: "world" });
await client("inputNotNeeded");
});
================================================
FILE: src/tests/repl.test.ts
================================================
import { it } from "vitest";
import { initUntypeable } from "../untypeable";
it("REPL", () => {
const u = initUntypeable().pushArg<"GET" | "POST">();
const router = u.router({
something: {
GET: u.output<string>(),
POST: u.input<string>().output<string>(),
},
});
});
================================================
FILE: src/tests/router-definition.test.ts
================================================
import { describe, expect, it } from "vitest";
import { initUntypeable } from "../untypeable";
it("Should not let you pass an input directly to a route", () => {
const u = initUntypeable();
expect(() =>
u.router({
// @ts-expect-error
inputNeeded: u.input<string>(),
}),
).toThrowError();
});
it("Should let you pass an output without an input", () => {
const u = initUntypeable();
u.router({
noInputNeeded: u.output<string>(),
});
});
================================================
FILE: src/tests/type-only.test.ts
================================================
import { describe, expect, it, vitest } from "vitest";
import { createTypeLevelClient } from "../client";
import { initUntypeable } from "../untypeable";
import { Equal, Expect } from "./utils";
const u = initUntypeable();
const router = u.router({
inputNeeded: u.input<string>().output<number>(),
noInputNeeded: u.output<boolean>(),
partialInputOnly: u.input<{ a?: string }>().output<{ a: string }>(),
});
describe("input types", () => {
it("Should require an input of the correct type if one has been specified", async () => {
const fn = vitest.fn();
const client = createTypeLevelClient<typeof router>(fn);
// @ts-expect-error
await client("inputNeeded");
expect(fn).toHaveBeenLastCalledWith("inputNeeded");
await client("inputNeeded", "hello");
expect(fn).toHaveBeenLastCalledWith("inputNeeded", "hello");
// @ts-expect-error
await client("inputNeeded", 0);
expect(fn).toHaveBeenLastCalledWith("inputNeeded", 0);
});
it("Should not require an input if one has not been specified", async () => {
const fn = vitest.fn();
const client = createTypeLevelClient<typeof router>(fn);
await client("noInputNeeded");
expect(fn).toHaveBeenLastCalledWith("noInputNeeded");
// @ts-expect-error
await client("noInputNeeded", "hello");
expect(fn).toHaveBeenLastCalledWith("noInputNeeded", "hello");
});
it("Should not require an input if all properties of the input are optional", async () => {
const fn = vitest.fn();
const client = createTypeLevelClient<typeof router>(fn);
await client("partialInputOnly");
expect(fn).toHaveBeenLastCalledWith("partialInputOnly");
await client("partialInputOnly", {});
expect(fn).toHaveBeenLastCalledWith("partialInputOnly", {});
});
});
describe("Output types", () => {
it("Should return an output of the correct type", async () => {
const fn = vitest.fn();
const client = createTypeLevelClient<typeof router>(fn);
const numResult = await client("inputNeeded", "adwawd");
const boolResult = await client("noInputNeeded");
type tests = [
Expect<Equal<typeof numResult, number>>,
Expect<Equal<typeof boolResult, boolean>>,
];
});
});
================================================
FILE: src/tests/utils.ts
================================================
export type Expect<T extends true> = T;
export type ExpectTrue<T extends true> = T;
export type ExpectFalse<T extends false> = T;
export type IsTrue<T extends true> = T;
export type IsFalse<T extends false> = T;
export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <
T,
>() => T extends Y ? 1 : 2
? true
: false;
export type NotEqual<X, Y> = true extends Equal<X, Y> ? false : true;
// https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360
export type IsAny<T> = 0 extends 1 & T ? true : false;
export type NotAny<T> = true extends IsAny<T> ? false : true;
export type Debug<T> = { [K in keyof T]: T[K] };
export type MergeInsertions<T> = T extends object
? { [K in keyof T]: MergeInsertions<T[K]> }
: T;
export type Alike<X, Y> = Equal<MergeInsertions<X>, MergeInsertions<Y>>;
export type ExpectExtends<VALUE, EXPECTED> = EXPECTED extends VALUE
? true
: false;
export type ExpectValidArgs<
FUNC extends (...args: any[]) => any,
ARGS extends any[],
> = ARGS extends Parameters<FUNC> ? true : false;
export type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
export const doNotExecute = (func: () => any) => {};
================================================
FILE: src/tests/with-schemas.test.ts
================================================
import { expect, it, vitest } from "vitest";
import { z } from "zod";
import { createSafeClient } from "../safe-client";
import { initUntypeable } from "../untypeable";
it("Should let you define inputs and outputs as schemas", async () => {
const u = initUntypeable();
const router = u.router({
inputNeeded: u.input(z.string()).output(z.object({ hello: z.string() })),
});
const fn = vitest.fn();
const client = createSafeClient(router, fn);
fn.mockResolvedValueOnce({ hello: "world" });
await client("inputNeeded", "hello");
});
it("Should error if you do not provide the correct input", async () => {
const u = initUntypeable();
const router = u.router({
inputNeeded: u.input(z.string()).output(z.object({ hello: z.string() })),
});
const fn = vitest.fn();
const client = createSafeClient(router, fn);
fn.mockResolvedValueOnce({ hello: "world" });
await client("inputNeeded", "hello");
});
it("Should error if the fetcher does not provide the correct output WITHOUT an input", async () => {
const u = initUntypeable();
const router = u.router({
inputNotNeeded: u.output(z.object({ hello: z.string() })),
});
const fn = vitest.fn();
const client = createSafeClient(router, fn);
fn.mockResolvedValueOnce({ hello: 124 });
await expect(() => client("inputNotNeeded")).rejects.toThrowError();
});
it("Should error if the fetcher does not provide the correct output WITH an input", async () => {
const u = initUntypeable();
const router = u.router({
inputNeeded: u.input(z.string()).output(z.object({ hello: z.string() })),
});
const fn = vitest.fn();
const client = createSafeClient(router, fn);
fn.mockResolvedValueOnce({ hello: 124 });
await expect(() => client("inputNeeded", "124123123")).rejects.toThrowError();
});
================================================
FILE: src/types.ts
================================================
type DefaultArgs = [string];
export type AcceptedParser<T> =
| ((input: unknown) => T)
| {
parse: (input: unknown) => T;
};
export type SchemaMap = Map<string, Schemas>;
export type Schemas = {
input: AcceptedParser<any> | undefined;
output: AcceptedParser<any> | undefined;
};
export interface UntypeableBase<TArgs extends readonly string[] = DefaultArgs> {
pushArg: <TArg extends string>() => UntypeableBase<[...TArgs, TArg]>;
unshiftArg: <TArg extends string>() => UntypeableBase<[TArg, ...TArgs]>;
args<TArg1 extends string>(): UntypeableBase<[TArg1]>;
args<TArg1 extends string, TArg2 extends string>(): UntypeableBase<
[TArg1, TArg2]
>;
args<
TArg1 extends string,
TArg2 extends string,
TArg3 extends string,
>(): UntypeableBase<[TArg1, TArg2, TArg3]>;
args<
TArg1 extends string,
TArg2 extends string,
TArg3 extends string,
TArg4 extends string,
>(): UntypeableBase<[TArg1, TArg2, TArg3, TArg4]>;
args<TArgs extends readonly string[]>(): UntypeableBase<TArgs>;
router: <
TNewRoutes extends StringArrayToObject<TArgs, UntypeableOutput<any, any>>,
>(
routes: TNewRoutes,
) => UntypeableRouter<TArgs, Prettify<TNewRoutes>>;
input: <TInput>(parser?: AcceptedParser<TInput>) => UntypeableInput<TInput>;
output: <TOutput>(
parser?: AcceptedParser<TOutput>,
) => UntypeableOutput<{}, TOutput>;
}
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
export interface UntypeableInput<TInput> {
__type: "input";
output: <TOutput>(
schema?: AcceptedParser<TOutput>,
) => UntypeableOutput<TInput, TOutput>;
inputSchema: AcceptedParser<TInput> | undefined;
}
export interface UntypeableOutput<TInput, TOutput> {
__type: "output";
outputSchema: AcceptedParser<TOutput> | undefined;
inputSchema: AcceptedParser<TInput> | undefined;
}
export interface UntypeableRouter<
TArgs extends readonly string[] = DefaultArgs,
TRoutes extends Record<string, any> = {},
> {
_schemaMap: SchemaMap;
add: <
TNewRoutes extends StringArrayToObject<TArgs, UntypeableOutput<any, any>>,
>(
routes: TNewRoutes,
) => UntypeableRouter<TArgs, Prettify<TRoutes & TNewRoutes>>;
merge: <TRoutes2 extends Record<string, any>>(
router: UntypeableRouter<TArgs, TRoutes2>,
) => UntypeableRouter<TArgs, Prettify<TRoutes & TRoutes2>>;
}
export type LooseUntypeableHandler<TArgs extends readonly string[]> = (
...args: [...args: TArgs, input: any]
) => Promise<any>;
export type UntypeableHandler<
TArgs extends readonly string[] = DefaultArgs,
TRoutes extends Record<string, any> = {},
> = TArgs["length"] extends 1
? <T1 extends keyof TRoutes>(
firstArg: T1,
...args: ArgsFromRoutes<TRoutes[T1]>
) => TRoutes[T1] extends UntypeableOutput<any, infer TOutput>
? Promise<TOutput>
: never
: TArgs["length"] extends 2
? <T1 extends keyof TRoutes, T2 extends keyof TRoutes[T1]>(
firstArg: T1,
secondArg: T2,
...args: ArgsFromRoutes<TRoutes[T1][T2]>
) => TRoutes[T1][T2] extends UntypeableOutput<any, infer TOutput>
? Promise<TOutput>
: never
: TArgs["length"] extends 3
? <
T1 extends keyof TRoutes,
T2 extends keyof TRoutes[T1],
T3 extends keyof TRoutes[T1][T2],
>(
firstArg: T1,
secondArg: T2,
thirdArg: T3,
...args: ArgsFromRoutes<TRoutes[T1][T2][T3]>
) => TRoutes[T1][T2][T3] extends UntypeableOutput<any, infer TOutput>
? Promise<TOutput>
: never
: TArgs["length"] extends 4
? <
T1 extends keyof TRoutes,
T2 extends keyof TRoutes[T1],
T3 extends keyof TRoutes[T1][T2],
T4 extends keyof TRoutes[T1][T2][T3],
>(
firstArg: T1,
secondArg: T2,
thirdArg: T3,
fourthArg: T4,
...args: ArgsFromRoutes<TRoutes[T1][T2][T3][T4]>
) => TRoutes[T1][T2][T3][T4] extends UntypeableOutput<any, infer TOutput>
? Promise<TOutput>
: never
: never;
export type ArgsFromRoutes<TRoutes> = TRoutes extends UntypeableOutput<
infer TInput,
any
>
? TInput extends Record<string, never>
? []
: Record<string, undefined> extends TInput
? [input?: TInput]
: [input: Prettify<TInput>]
: never;
export type StringArrayToObject<
T extends readonly string[],
TValue,
> = T extends [
infer THead extends string,
...infer TTail extends readonly string[],
]
? { [K in THead]?: StringArrayToObject<TTail, TValue> }
: TValue;
export type ArgsFromRouter<TRouter extends UntypeableRouter<any, any>> =
TRouter extends UntypeableRouter<infer TArgs, any> ? TArgs : never;
export type RoutesFromRouter<TRouter extends UntypeableRouter<any, any>> =
TRouter extends UntypeableRouter<any, infer TRoutes> ? TRoutes : never;
================================================
FILE: src/untypeable.ts
================================================
import { SEPARATOR } from "./constants";
import {
UntypeableBase,
UntypeableInput,
UntypeableOutput,
UntypeableRouter,
AcceptedParser,
SchemaMap,
} from "./types";
const isOutput = (value: {
__type?: string;
}): value is UntypeableOutput<any, any> => {
return value.__type === "output";
};
const isInput = (value: {
__type?: string;
}): value is UntypeableOutput<any, any> => {
return value.__type === "input";
};
const combineMaps = <K, V>(map1: Map<K, V>, map2: Map<K, V>) => {
const newMap = new Map(map1);
for (const [key, value] of map2) {
newMap.set(key, value);
}
return newMap;
};
const initRouter = <TRoutes extends Record<string, any> = {}>(
routes: TRoutes,
schemaMap: SchemaMap = new Map(),
): UntypeableRouter => {
const collectRoutes = (
routes: Record<string, UntypeableOutput<any, any> | Record<string, any>>,
path: string[] = [],
) => {
for (const [key, value] of Object.entries(routes)) {
if (isOutput(value)) {
schemaMap.set([...path, key].join(SEPARATOR), {
input: value.inputSchema,
output: value.outputSchema,
});
} else if (isInput(value)) {
throw new Error("An input cannot be passed directly to a router.");
} else {
collectRoutes(value, [...path, key]);
}
}
};
collectRoutes(routes);
return {
add: (newRoutes) => initRouter(newRoutes, schemaMap),
merge: (router) =>
initRouter({}, combineMaps(router._schemaMap, schemaMap)),
_schemaMap: schemaMap,
};
};
const initInput = (
inputSchema: AcceptedParser<any> | undefined,
): UntypeableInput<any> => {
return {
output: (outputSchema) => initOutput(inputSchema, outputSchema),
inputSchema,
__type: "input",
};
};
const initOutput = (
inputSchema: AcceptedParser<any> | undefined,
outputSchema: AcceptedParser<any> | undefined,
): UntypeableOutput<any, any> => {
return {
__type: "output",
inputSchema,
outputSchema,
};
};
export const initUntypeable = (): UntypeableBase => {
return {
pushArg: () => initUntypeable() as any,
unshiftArg: () => initUntypeable() as any,
args: () => initUntypeable() as any,
input: initInput,
output: (outputSchema) => initOutput(undefined, outputSchema),
router: (routes) => initRouter(routes),
};
};
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "NodeNext", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
"noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"exclude": [
"dist"
]
}
================================================
FILE: tsup.config.ts
================================================
import { defineConfig } from "tsup";
const config = defineConfig({
format: ["cjs", "esm"],
dts: true,
entry: {
index: "src/index.ts",
client: "src/client.ts",
},
});
export default config;
gitextract_qcn3yl_o/ ├── .changeset/ │ ├── README.md │ └── config.json ├── .github/ │ └── workflows/ │ ├── main.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── docs/ │ └── swapi-example/ │ ├── swapi-example.ts │ └── types.ts ├── package.json ├── readme.md ├── src/ │ ├── client.ts │ ├── constants.ts │ ├── index.ts │ ├── playground.ts │ ├── safe-client.ts │ ├── tests/ │ │ ├── add.test.ts │ │ ├── dynamic-args.test.ts │ │ ├── merge.test.ts │ │ ├── repl.test.ts │ │ ├── router-definition.test.ts │ │ ├── type-only.test.ts │ │ ├── utils.ts │ │ └── with-schemas.test.ts │ ├── types.ts │ └── untypeable.ts ├── tsconfig.json └── tsup.config.ts
SYMBOL INDEX (38 symbols across 6 files)
FILE: docs/swapi-example/types.ts
type Person (line 1) | type Person = {
type Planet (line 13) | type Planet = {
type Film (line 23) | type Film = {
type Vehicle (line 36) | type Vehicle = {
type Paginated (line 45) | type Paginated<T> = {
FILE: src/constants.ts
constant SEPARATOR (line 1) | const SEPARATOR = "__";
FILE: src/playground.ts
type User (line 6) | type User = {
FILE: src/tests/type-only.test.ts
type tests (line 68) | type tests = [
FILE: src/tests/utils.ts
type Expect (line 1) | type Expect<T extends true> = T;
type ExpectTrue (line 2) | type ExpectTrue<T extends true> = T;
type ExpectFalse (line 3) | type ExpectFalse<T extends false> = T;
type IsTrue (line 4) | type IsTrue<T extends true> = T;
type IsFalse (line 5) | type IsFalse<T extends false> = T;
type Equal (line 7) | type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <
type NotEqual (line 12) | type NotEqual<X, Y> = true extends Equal<X, Y> ? false : true;
type IsAny (line 15) | type IsAny<T> = 0 extends 1 & T ? true : false;
type NotAny (line 16) | type NotAny<T> = true extends IsAny<T> ? false : true;
type Debug (line 18) | type Debug<T> = { [K in keyof T]: T[K] };
type MergeInsertions (line 19) | type MergeInsertions<T> = T extends object
type Alike (line 23) | type Alike<X, Y> = Equal<MergeInsertions<X>, MergeInsertions<Y>>;
type ExpectExtends (line 25) | type ExpectExtends<VALUE, EXPECTED> = EXPECTED extends VALUE
type ExpectValidArgs (line 28) | type ExpectValidArgs<
type UnionToIntersection (line 33) | type UnionToIntersection<U> = (
FILE: src/types.ts
type DefaultArgs (line 1) | type DefaultArgs = [string];
type AcceptedParser (line 3) | type AcceptedParser<T> =
type SchemaMap (line 9) | type SchemaMap = Map<string, Schemas>;
type Schemas (line 11) | type Schemas = {
type UntypeableBase (line 16) | interface UntypeableBase<TArgs extends readonly string[] = DefaultArgs> {
type Prettify (line 46) | type Prettify<T> = {
type UntypeableInput (line 50) | interface UntypeableInput<TInput> {
type UntypeableOutput (line 58) | interface UntypeableOutput<TInput, TOutput> {
type UntypeableRouter (line 64) | interface UntypeableRouter<
type LooseUntypeableHandler (line 80) | type LooseUntypeableHandler<TArgs extends readonly string[]> = (
type UntypeableHandler (line 84) | type UntypeableHandler<
type ArgsFromRoutes (line 132) | type ArgsFromRoutes<TRoutes> = TRoutes extends UntypeableOutput<
type StringArrayToObject (line 143) | type StringArrayToObject<
type ArgsFromRouter (line 153) | type ArgsFromRouter<TRouter extends UntypeableRouter<any, any>> =
type RoutesFromRouter (line 156) | type RoutesFromRouter<TRouter extends UntypeableRouter<any, any>> =
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (47K chars).
[
{
"path": ".changeset/README.md",
"chars": 510,
"preview": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that wo"
},
{
"path": ".changeset/config.json",
"chars": 271,
"preview": "{\n \"$schema\": \"https://unpkg.com/@changesets/config@2.3.0/schema.json\",\n \"changelog\": \"@changesets/cli/changelog\",\n \""
},
{
"path": ".github/workflows/main.yml",
"chars": 422,
"preview": "name: CI\non:\n push:\n branches:\n - \"**\"\n pull_request:\n branches:\n - \"**\"\n\njobs:\n build:\n runs-on: "
},
{
"path": ".github/workflows/publish.yml",
"chars": 690,
"preview": "name: Publish\non:\n push:\n branches:\n - \"main\"\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\njobs:\n p"
},
{
"path": ".gitignore",
"chars": 17,
"preview": "node_modules\ndist"
},
{
"path": ".npmignore",
"chars": 45,
"preview": "src\nnode_modules\npnpm-lock.yaml\ntsconfig.json"
},
{
"path": "CHANGELOG.md",
"chars": 255,
"preview": "# untypeable\n\n## 0.2.0\n\n### Minor Changes\n\n- 9db7f9b: Made it so you don't need to pass an empty object to the client if"
},
{
"path": "docs/swapi-example/swapi-example.ts",
"chars": 1262,
"preview": "import { createTypeLevelClient, initUntypeable } from \"../../src/index\";\nimport { Person, Paginated, Planet, Film, Vehic"
},
{
"path": "docs/swapi-example/types.ts",
"chars": 895,
"preview": "export type Person = {\n name: string;\n height: string;\n mass: string;\n hair_color: string;\n skin_color: string;\n b"
},
{
"path": "package.json",
"chars": 1032,
"preview": "{\n \"name\": \"untypeable\",\n \"version\": \"0.2.1\",\n \"description\": \"Get type-safe access to any API, with a zero-bundle si"
},
{
"path": "readme.md",
"chars": 7576,
"preview": "# 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 t"
},
{
"path": "src/client.ts",
"chars": 857,
"preview": "import {\n ArgsFromRouter,\n LooseUntypeableHandler,\n RoutesFromRouter,\n UntypeableHandler,\n UntypeableRouter,\n} from"
},
{
"path": "src/constants.ts",
"chars": 31,
"preview": "export const SEPARATOR = \"__\";\n"
},
{
"path": "src/index.ts",
"chars": 112,
"preview": "export * from \"./client\";\nexport * from \"./types\";\nexport * from \"./untypeable\";\nexport * from \"./safe-client\";\n"
},
{
"path": "src/playground.ts",
"chars": 684,
"preview": "import { initUntypeable, createTypeLevelClient } from \"untypeable\";\n\n// Initialize untypeable\nconst u = initUntypeable()"
},
{
"path": "src/safe-client.ts",
"chars": 1827,
"preview": "import { SEPARATOR } from \"./constants\";\nimport {\n AcceptedParser,\n ArgsFromRouter,\n LooseUntypeableHandler,\n Routes"
},
{
"path": "src/tests/add.test.ts",
"chars": 774,
"preview": "import { expect, it, vitest } from \"vitest\";\nimport { z } from \"zod\";\nimport { createSafeClient } from \"../safe-client\";"
},
{
"path": "src/tests/dynamic-args.test.ts",
"chars": 866,
"preview": "import { it, vitest } from \"vitest\";\nimport { createTypeLevelClient } from \"../client\";\nimport { initUntypeable } from \""
},
{
"path": "src/tests/merge.test.ts",
"chars": 826,
"preview": "import { expect, it, vitest } from \"vitest\";\nimport { z } from \"zod\";\nimport { createSafeClient } from \"../safe-client\";"
},
{
"path": "src/tests/repl.test.ts",
"chars": 295,
"preview": "import { it } from \"vitest\";\nimport { initUntypeable } from \"../untypeable\";\n\nit(\"REPL\", () => {\n const u = initUntypea"
},
{
"path": "src/tests/router-definition.test.ts",
"chars": 475,
"preview": "import { describe, expect, it } from \"vitest\";\nimport { initUntypeable } from \"../untypeable\";\n\nit(\"Should not let you p"
},
{
"path": "src/tests/type-only.test.ts",
"chars": 2228,
"preview": "import { describe, expect, it, vitest } from \"vitest\";\nimport { createTypeLevelClient } from \"../client\";\nimport { initU"
},
{
"path": "src/tests/utils.ts",
"chars": 1249,
"preview": "export type Expect<T extends true> = T;\nexport type ExpectTrue<T extends true> = T;\nexport type ExpectFalse<T extends fa"
},
{
"path": "src/tests/with-schemas.test.ts",
"chars": 1810,
"preview": "import { expect, it, vitest } from \"vitest\";\nimport { z } from \"zod\";\nimport { createSafeClient } from \"../safe-client\";"
},
{
"path": "src/types.ts",
"chars": 4755,
"preview": "type DefaultArgs = [string];\n\nexport type AcceptedParser<T> =\n | ((input: unknown) => T)\n | {\n parse: (input: unk"
},
{
"path": "src/untypeable.ts",
"chars": 2339,
"preview": "import { SEPARATOR } from \"./constants\";\nimport {\n UntypeableBase,\n UntypeableInput,\n UntypeableOutput,\n UntypeableR"
},
{
"path": "tsconfig.json",
"chars": 11111,
"preview": "{\n \"compilerOptions\": {\n /* Visit https://aka.ms/tsconfig to read more about this file */\n /* Projects */\n // "
},
{
"path": "tsup.config.ts",
"chars": 207,
"preview": "import { defineConfig } from \"tsup\";\n\nconst config = defineConfig({\n format: [\"cjs\", \"esm\"],\n dts: true,\n entry: {\n "
}
]
About this extraction
This page contains the full source code of the total-typescript/untypeable GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (42.4 KB), approximately 11.5k tokens, and a symbol index with 38 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.