Skip to content

Commit eaf2aec

Browse files
committed
Initial commit
0 parents  commit eaf2aec

File tree

16 files changed

+2064
-0
lines changed

16 files changed

+2064
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# typed-fetch
2+
3+
typed-fetch is intended to be a drop-in replacement for [openapi-typescript/openapi-fetch](https://github.com/openapi-ts/openapi-typescript) but with a much simpler TypeScript implementation that I believe results in fewer issues and enhanced readability.
4+
5+
## Quickstart
6+
7+
```bash
8+
# Generate TypeScript types from OpenAPI document
9+
typed-fetch --openapi examples/petstore-openapi.yaml --output petstore-openapi.ts
10+
```
11+
12+
```ts
13+
// Use the generated library in your .ts files
14+
import { createClient } from "./petstore-openapi";
15+
16+
const client = createClient({ baseUrl: "https://petstore.swagger.io/v2" });
17+
18+
const { data, error } = await client.POST("/store/order", {
19+
body: {
20+
id: 10,
21+
petId: 198772,
22+
quantity: 7,
23+
shipDate: "2021-07-07T00:00:00.000Z",
24+
status: "approved",
25+
complete: true
26+
}
27+
});
28+
```
29+
30+
## Installation
31+
32+
You can download pre-built binaries from [Releases](https://github.com/RPGillespie6/typed-fetch/releases).
33+
34+
If you have Go installed, you can download and install the latest version directly with:
35+
36+
```bash
37+
go install github.com/RPGillespie6/typed-fetch@latest
38+
```
39+
40+
Currently this utility is not being published to npm, but it's a future possibility.
41+
42+
# Overview
43+
44+
Type-checked [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) calls using OpenAPI + TypeScript.
45+
46+
Mostly API-compatible with [openapi-fetch](https://github.com/openapi-ts/openapi-typescript).
47+
48+
Why create this library, if it's nearly identical to [openapi-fetch](https://github.com/openapi-ts/openapi-typescript)? It's because openapi-fetch uses a complex generics/constraints-based TypeScript implementation for type checking, which in my opinion makes it *very* difficult to understand, test, and maintain. The goal of typed-fetch, on the other hand, is to generate simple, straightforward types given an OpenAPI document so that any generalist programmer could inspect the generated TypeScript and easiy understand it -- [see for yourself, no PhD in TypeScript required](examples/petstore-openapi.ts).
49+
50+
TypeScript generated with this utility is composed almost entirely of type definitions which are stripped out at compile time, resulting in an extremely lightweight fetch wrapper. As a result, typed-fetch should have the same size and performance characteristics as openapi-fetch.
51+
52+
Features:
53+
- Generated TypeScript definitions are at least an order of magnitude simpler and more straightforward than openapi-fetch, which means you can easily jump to and inspect type definitions in your favorite IDE without fear
54+
- Arbitrary combinations of required and optional parameters in request bodies are correctly type-checked (broken in openapi-fetch as of July 2024)
55+
- View your OpenAPI documentation in VSCode when you hover on functions and property names
56+
- Like esbuild, typed-fetch is written in golang, so it's lightning fast
57+
58+
Limitations:
59+
- Only OpenAPI 3.1+ specifications officially supported (see [migration guide](https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0)), though most OpenAPI 3.0 documents will also work.
60+
- Some OpenAPI 3 features not currently implemented, especially if it's unclear how it would map to a TypeScript API.
61+
62+
Missing functionality?
63+
64+
Please open an issue with the following 2 things:
65+
- Snippet of valid OpenAPI 3 yaml
66+
- Expected TypeScript to be generated
67+
68+
Also, I'm open to the idea of migrating this utility/approach *into* openapi-fetch if the maintainer(s) there are on-board; I'd rather there be one great tool everyone uses than further fragment the space.

examples/petstore-openapi.ts

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
// Code generated by typed-fetch. DO NOT EDIT.
2+
// https://github.com/RPGillespie6/typed-fetch
3+
4+
// URL types
5+
type UrlDeletePetPetId = '/pet/{petId}';
6+
type UrlDeleteStoreOrderOrderId = '/store/order/{orderId}';
7+
type UrlDeleteUserUsername = '/user/{username}';
8+
type UrlValidDelete = UrlDeletePetPetId | UrlDeleteStoreOrderOrderId | UrlDeleteUserUsername;
9+
10+
type UrlGetPetFindByStatus = '/pet/findByStatus';
11+
type UrlGetPetFindByTags = '/pet/findByTags';
12+
type UrlGetPetPetId = '/pet/{petId}';
13+
type UrlGetStoreInventory = '/store/inventory';
14+
type UrlGetStoreOrderOrderId = '/store/order/{orderId}';
15+
type UrlGetUserLogin = '/user/login';
16+
type UrlGetUserLogout = '/user/logout';
17+
type UrlGetUserUsername = '/user/{username}';
18+
type UrlValidGet = UrlGetPetFindByStatus | UrlGetPetFindByTags | UrlGetPetPetId | UrlGetStoreInventory | UrlGetStoreOrderOrderId | UrlGetUserLogin | UrlGetUserLogout | UrlGetUserUsername;
19+
20+
type UrlPostPet = '/pet';
21+
type UrlPostPetPetId = '/pet/{petId}';
22+
type UrlPostPetPetIdUploadImage = '/pet/{petId}/uploadImage';
23+
type UrlPostStoreOrder = '/store/order';
24+
type UrlPostUser = '/user';
25+
type UrlPostUserCreateWithList = '/user/createWithList';
26+
type UrlValidPost = UrlPostPet | UrlPostPetPetId | UrlPostPetPetIdUploadImage | UrlPostStoreOrder | UrlPostUser | UrlPostUserCreateWithList;
27+
28+
type UrlPutPet = '/pet';
29+
type UrlPutUserUsername = '/user/{username}';
30+
type UrlValidPut = UrlPutPet | UrlPutUserUsername;
31+
32+
// Component types
33+
34+
type ComponentSchemaAddress = {
35+
/** Example: Palo Alto */
36+
city?: string;
37+
/** Example: CA */
38+
state?: string;
39+
/** Example: 437 Lytton */
40+
street?: string;
41+
/** Example: 94301 */
42+
zip?: string;
43+
}
44+
45+
type ComponentSchemaApiResponse = {
46+
code?: number;
47+
message?: string;
48+
type?: string;
49+
}
50+
51+
type ComponentSchemaCategory = {
52+
id?: number;
53+
/** Example: Dogs */
54+
name?: string;
55+
}
56+
57+
type ComponentSchemaCustomer = {
58+
address?: ComponentSchemaAddress[];
59+
id?: number;
60+
/** Example: fehguy */
61+
username?: string;
62+
}
63+
64+
type ComponentSchemaOrder = {
65+
complete?: boolean;
66+
id?: number;
67+
petId?: number;
68+
quantity?: number;
69+
shipDate?: string;
70+
/** Order Status; Example: approved */
71+
status?: 'placed' | 'approved' | 'delivered';
72+
}
73+
74+
type ComponentSchemaPet = {
75+
category?: ComponentSchemaCategory;
76+
id?: number;
77+
/** Example: doggie */
78+
name: string;
79+
photoUrls: string[];
80+
/** pet status in the store */
81+
status?: 'available' | 'pending' | 'sold';
82+
tags?: ComponentSchemaTag[];
83+
}
84+
85+
type ComponentSchemaTag = {
86+
id?: number;
87+
name?: string;
88+
}
89+
90+
type ComponentSchemaUser = {
91+
/** Example: [email protected] */
92+
email?: string;
93+
/** Example: John */
94+
firstName?: string;
95+
id?: number;
96+
/** Example: James */
97+
lastName?: string;
98+
/** Example: 12345 */
99+
password?: string;
100+
/** Example: 12345 */
101+
phone?: string;
102+
/** User Status */
103+
userStatus?: number;
104+
/** Example: theUser */
105+
username?: string;
106+
}
107+
108+
// Request types
109+
110+
// PUT /pet
111+
type BodyPutPet = ComponentSchemaPet;
112+
type RequestPutPet = Omit<RequestInit, 'body'> & { body: BodyPutPet; };
113+
114+
// POST /pet
115+
type BodyPostPet = ComponentSchemaPet;
116+
type RequestPostPet = Omit<RequestInit, 'body'> & { body: BodyPostPet; };
117+
118+
// GET /pet/findByStatus
119+
type ParamGetPetFindByStatus = {
120+
query?: {
121+
status?: 'available' | 'pending' | 'sold';
122+
};
123+
}
124+
type RequestGetPetFindByStatus = RequestInit & { params?: ParamGetPetFindByStatus; };
125+
126+
// GET /pet/findByTags
127+
type ParamGetPetFindByTags = {
128+
query?: {
129+
tags?: string[];
130+
};
131+
}
132+
type RequestGetPetFindByTags = RequestInit & { params?: ParamGetPetFindByTags; };
133+
134+
// GET /pet/{petId}
135+
type ParamGetPetPetId = {
136+
path: {
137+
petId: number;
138+
};
139+
}
140+
type RequestGetPetPetId = RequestInit & { params: ParamGetPetPetId; };
141+
142+
// POST /pet/{petId}
143+
type ParamPostPetPetId = {
144+
path: {
145+
petId: number;
146+
};
147+
query?: {
148+
name?: string;
149+
status?: string;
150+
};
151+
}
152+
type RequestPostPetPetId = RequestInit & { params: ParamPostPetPetId; };
153+
154+
// DELETE /pet/{petId}
155+
type ParamDeletePetPetId = {
156+
header?: {
157+
api_key?: string;
158+
};
159+
path: {
160+
petId: number;
161+
};
162+
}
163+
type RequestDeletePetPetId = RequestInit & { params: ParamDeletePetPetId; };
164+
165+
// POST /pet/{petId}/uploadImage
166+
type ParamPostPetPetIdUploadImage = {
167+
path: {
168+
petId: number;
169+
};
170+
query?: {
171+
additionalMetadata?: string;
172+
};
173+
}
174+
type BodyPostPetPetIdUploadImage = string;
175+
type RequestPostPetPetIdUploadImage = Omit<RequestInit, 'body'> & { params: ParamPostPetPetIdUploadImage; body?: BodyPostPetPetIdUploadImage; };
176+
177+
// GET /store/inventory
178+
type RequestGetStoreInventory = RequestInit;
179+
180+
// POST /store/order
181+
type BodyPostStoreOrder = ComponentSchemaOrder;
182+
type RequestPostStoreOrder = Omit<RequestInit, 'body'> & { body?: BodyPostStoreOrder; };
183+
184+
// GET /store/order/{orderId}
185+
type ParamGetStoreOrderOrderId = {
186+
path: {
187+
orderId: number;
188+
};
189+
}
190+
type RequestGetStoreOrderOrderId = RequestInit & { params: ParamGetStoreOrderOrderId; };
191+
192+
// DELETE /store/order/{orderId}
193+
type ParamDeleteStoreOrderOrderId = {
194+
path: {
195+
orderId: number;
196+
};
197+
}
198+
type RequestDeleteStoreOrderOrderId = RequestInit & { params: ParamDeleteStoreOrderOrderId; };
199+
200+
// POST /user
201+
type BodyPostUser = ComponentSchemaUser;
202+
type RequestPostUser = Omit<RequestInit, 'body'> & { body?: BodyPostUser; };
203+
204+
// POST /user/createWithList
205+
type BodyPostUserCreateWithList = ComponentSchemaUser[];
206+
type RequestPostUserCreateWithList = Omit<RequestInit, 'body'> & { body?: BodyPostUserCreateWithList; };
207+
208+
// GET /user/login
209+
type ParamGetUserLogin = {
210+
query?: {
211+
username?: string;
212+
password?: string;
213+
};
214+
}
215+
type RequestGetUserLogin = RequestInit & { params?: ParamGetUserLogin; };
216+
217+
// GET /user/logout
218+
type RequestGetUserLogout = RequestInit;
219+
220+
// GET /user/{username}
221+
type ParamGetUserUsername = {
222+
path: {
223+
username: string;
224+
};
225+
}
226+
type RequestGetUserUsername = RequestInit & { params: ParamGetUserUsername; };
227+
228+
// PUT /user/{username}
229+
type ParamPutUserUsername = {
230+
path: {
231+
username: string;
232+
};
233+
}
234+
type BodyPutUserUsername = ComponentSchemaUser;
235+
type RequestPutUserUsername = Omit<RequestInit, 'body'> & { params: ParamPutUserUsername; body?: BodyPutUserUsername; };
236+
237+
// DELETE /user/{username}
238+
type ParamDeleteUserUsername = {
239+
path: {
240+
username: string;
241+
};
242+
}
243+
type RequestDeleteUserUsername = RequestInit & { params: ParamDeleteUserUsername; };
244+
245+
type DataResponse<D> = { data: D; response: Response; };
246+
type ErrorResponse<E> = { error: E; response: Response; };
247+
type FetchResponse<D, E> = DataResponse<D> | ErrorResponse<E>;
248+
249+
interface ClientOptions extends RequestInit {
250+
baseUrl?: string;
251+
252+
// Override fetch function (useful for testing)
253+
fetch?: (input: Request) => Promise<Response>;
254+
255+
// global body serializer -- allows you to customize how the body is serialized before sending
256+
// normally not needed unless you are using something like XML instead of JSON
257+
bodySerializer?: (body: any) => BodyInit | null;
258+
259+
// global query serializer -- allows you to customize how the query is serialized before sending
260+
// normally not needed unless you are using some custom array serialization like {foo: [1,2,3,4]} => ?foo=1;2;3;4
261+
querySerializer?: (query: any) => string;
262+
}
263+
264+
interface Client {
265+
// GET(url: string, init?: RequestInit): Promise<FetchResponse<Foo, Err>>;
266+
// POST(url: string, init?: BarRequestInit): Promise<FetchResponse<Bar, Err>>;
267+
}
268+
269+
// Client Implementation
270+
271+
// If not specified, default to application/json
272+
// Used to deduce body serializer and to set content-type header
273+
const contentTypeMap: Record<string, string> = {
274+
// "/some/binary/url": "application/octet-stream",
275+
}
276+
277+
class ClientImpl {
278+
options: ClientOptions;
279+
280+
constructor(options: ClientOptions) {
281+
options.baseUrl = options.baseUrl || ""; // Make sure baseUrl is always a string
282+
this.options = options;
283+
}
284+
285+
#fetch(input: Request): Promise<Response> {
286+
return (this.options.fetch || fetch)(input);
287+
}
288+
}
289+
290+
export function createClient(options: ClientOptions): Client {
291+
return new ClientImpl(options);
292+
}

0 commit comments

Comments
 (0)