Skip to content

Commit fb3f9a9

Browse files
authored
Allow to omit init arg of fetch (#162)
* Allow to omit init arg of fetch * Update comment
1 parent f6deb86 commit fb3f9a9

File tree

2 files changed

+76
-20
lines changed

2 files changed

+76
-20
lines changed

pkgs/typed-api-spec/src/fetch/index.t-test.ts

+35-1
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,22 @@ type ValidateUrlTestCase = [
4545
};
4646
};
4747
};
48+
"/users2": {
49+
get: {
50+
headers: { "x-foo"?: string; "x-bar"?: string };
51+
responses: { 200: { body: { prop: string } } };
52+
};
53+
};
4854
}>;
4955
(async () => {
5056
const f = fetch as FetchT<"", Spec>;
5157
{
52-
// @ts-expect-error 今はinitの省略ができないが、できるようにしたい
58+
// get methodが定義されており、headersが必要ない場合、Initは省略可能
5359
await f("/users");
5460

61+
// headersが定義されていても、すべて省略可能な場合はInitも省略可能
62+
await f("/users2");
63+
5564
// methodを省略した場合はgetとして扱う
5665
const res = await f("/users", {});
5766
(await res.json()).prop;
@@ -63,6 +72,31 @@ type ValidateUrlTestCase = [
6372
}
6473
})();
6574
}
75+
{
76+
type Spec = DefineApiEndpoints<{
77+
"/users": {
78+
get: {
79+
headers: { "x-foo"?: string; "Content-Type": "application/json" };
80+
responses: { 200: { body: { prop: string } } };
81+
};
82+
};
83+
"/users2": {
84+
post: {
85+
responses: { 200: { body: { prop: string } } };
86+
};
87+
};
88+
}>;
89+
(async () => {
90+
const f = fetch as FetchT<"", Spec>;
91+
{
92+
// @ts-expect-error getメソッドが定義されていても、headersが要求されている場合はInitは省略できない
93+
await f("/users");
94+
95+
// @ts-expect-error getメソッドが定義されていない場合、Initは省略できない
96+
await f("/users2");
97+
}
98+
})();
99+
}
66100
{
67101
type Spec = DefineApiEndpoints<{
68102
"/users": {

pkgs/typed-api-spec/src/fetch/index.ts

+41-19
Original file line numberDiff line numberDiff line change
@@ -75,25 +75,25 @@ export type RequestInitT<
7575
/**
7676
* FetchT is a type for window.fetch like function but more strict type information
7777
*
78-
* @template UrlPrefix - url prefix of `Input`
78+
* @template UrlPrefix - URL prefix of `Input`
7979
* For example, if `UrlPrefix` is "https://example.com", then `Input` must be `https://example.com/${string}`
8080
*
8181
* @template E - ApiEndpoints
8282
* E is used to infer the type of the acceptable path, response body, and more
8383
*/
8484
type FetchT<UrlPrefix extends UrlPrefixPattern, E extends ApiEndpoints> = <
8585
/**
86-
* internal type for FetchT
86+
* Internal type for FetchT
8787
* They are not supposed to be specified by the user
8888
*
89-
* @template UrlPattern - Acceptable url pattern
90-
* For example, if endpoints is defined as below:
89+
* @template UrlPattern - Acceptable URL pattern
90+
* For example, if endpoints are defined as below:
9191
* { "/users": ..., "/users/:userId": ... }
9292
* and UrlPrefix is "https://example.com",
9393
* then UrlPattern will be "https://example.com/users" | "https://example.com/users/${string}"
9494
*
9595
* @template Input - Input of the request by the user
96-
* For example, if endpoints is defined as below:
96+
* For example, if endpoints are defined as below:
9797
* { "/users": ..., "/users/:userId": ... }
9898
* then Input accepts "https://example.com/users" | "https://example.com/users/${string}"
9999
* If query is defined in the spec, Input also accepts "https://example.com/users?${string}" | "https://example.com/users/${string}?${string}"
@@ -102,27 +102,39 @@ type FetchT<UrlPrefix extends UrlPrefixPattern, E extends ApiEndpoints> = <
102102
* For example, if Input is "https://example.com/users/1", then InputPath will be "/users/${string}"
103103
*
104104
* @template CandidatePaths - Matched paths from `InputPath` and `keyof E`
105-
* For example, if InputPath is "/users/1" and endpoints is defined as below:
105+
* For example, if InputPath is "/users/1" and endpoints are defined as below:
106106
* { "/users": ..., "/users/:userId": ... }
107107
* then CandidatePaths will be "/users/:userId"
108108
* If no matched path is found, CandidatePaths will be never
109-
* If multiple matched paths are found, CandidatePaths will be union of matched paths
109+
* If multiple matched paths are found, CandidatePaths will be a union of matched paths
110110
*
111111
* @template AcceptableMethods - Acceptable methods for the matched path
112-
* For example, if CandidatePaths is "/users/:userId" and endpoints is defined as below:
112+
* For example, if CandidatePaths is "/users/:userId" and endpoints are defined as below:
113113
* { "/users": { get: ... }, "/users/:userId": { get: ..., post: ... } }
114114
* then AcceptableMethods will be "get" | "post"
115115
*
116-
* @template LM - Lowercase of `InputMethod`
116+
* @template LM - Lowercase version of `InputMethod`
117117
*
118-
* @template Query - Query object of endpoint which is matched with `CandidatePaths`
118+
* @template Query - Query object of the endpoint that matches `CandidatePaths`
119119
*
120-
* @template ResBody - Response body of the endpoint which is matched with `CandidatePaths`
120+
* @template Headers - Request headers object of the endpoint
121121
*
122-
* @template InputMethod - Method of the request specified by the user
122+
* @template Body - Request body object of the endpoint
123+
*
124+
* @template Response - Response object of the endpoint that matches `CandidatePaths`
125+
*
126+
* @template ValidatedUrl - Result of URL validation
127+
*
128+
* @template InputMethod - Method of the request as specified by the user
123129
* For example, if `fetch` is called with `fetch("https://example.com/users", { method: "post" })`,
124130
* then InputMethod will be "post".
125131
* If `get` method is defined in the spec, method can be omitted, and it will be `get` by default
132+
*
133+
* @template CanOmitMethod - Whether the method property in the "init" parameter can be omitted
134+
* If the endpoint defines a "get" method, then the method can be omitted
135+
*
136+
* @template CanOmitInit - Whether the "init" parameter can be omitted for the request
137+
* If the method can be omitted (`CanOmitMethod` is true) and the endpoint does not require headers, then the "init" parameter can be omitted
126138
*/
127139
UrlPattern extends ToUrlParamPattern<`${UrlPrefix}${keyof E & string}`>,
128140
Input extends Query extends undefined
@@ -141,6 +153,8 @@ type FetchT<UrlPrefix extends UrlPrefixPattern, E extends ApiEndpoints> = <
141153
: never,
142154
LM extends Lowercase<InputMethod>,
143155
Query extends ApiP<E, CandidatePaths, LM, "query">,
156+
Headers extends ApiP<E, CandidatePaths, LM, "headers">,
157+
Body extends ApiP<E, CandidatePaths, LM, "body">,
144158
Response extends ApiP<
145159
E,
146160
CandidatePaths,
@@ -154,17 +168,25 @@ type FetchT<UrlPrefix extends UrlPrefixPattern, E extends ApiEndpoints> = <
154168
AcceptableMethods,
155169
"get"
156170
>,
171+
CanOmitMethod extends boolean = "get" extends AcceptableMethods
172+
? true
173+
: false,
174+
CanOmitInit extends boolean = CanOmitMethod extends true
175+
? Headers extends undefined
176+
? true
177+
: Headers extends Record<string, string>
178+
? IsAllOptional<Headers> extends true
179+
? true
180+
: false
181+
: false
182+
: false,
157183
>(
158184
input: [ValidatedUrl] extends [C.OK | QueryParameterRequiredError]
159185
? Input
160186
: ValidatedUrl,
161-
init: RequestInitT<
162-
// If `get` method is defined in the spec, method can be omitted
163-
"get" extends AcceptableMethods ? true : false,
164-
ApiP<E, CandidatePaths, LM, "body">,
165-
ApiP<E, CandidatePaths, LM, "headers">,
166-
InputMethod
167-
>,
187+
...args: CanOmitInit extends true
188+
? [init?: RequestInitT<CanOmitMethod, Body, Headers, InputMethod>]
189+
: [init: RequestInitT<CanOmitMethod, Body, Headers, InputMethod>]
168190
) => Promise<Response>;
169191

170192
export default FetchT;

0 commit comments

Comments
 (0)