Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/common/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const Method = [
"head",
] as const;
export type Method = (typeof Method)[number];
export type CaseInsensitive<S extends string> = Uppercase<S> | Lowercase<S>;
export type CaseInsensitiveMethod = Method | Uppercase<Method>;
export const isMethod = (x: unknown): x is Method =>
Method.includes(x as Method);
Expand Down
36 changes: 32 additions & 4 deletions src/fetch/index.t-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ const JSONT = JSON as JSONT;
(async () => {
const f = fetch as FetchT<"", Spec>;
{
// TODO: 今はinitを省略する場合undefinedを明示的に渡す必要があるが、なんとかしたい
// TODO: 今はinitの省略ができないが、できるようにしたい
// methodを省略した場合はgetとして扱う
const res = await f("/users", undefined);
const res = await f("/users", {});
(await res.json()).prop;
}
})();
Expand Down Expand Up @@ -110,7 +110,7 @@ const JSONT = JSON as JSONT;
}

{
// TODO: 今は定義していないメソッドを受け付けてしまうが、いつかなんとかしたい
// @ts-expect-error 定義されていないmethodは指定できない
await f("/users", { method: "patch" });
}
})();
Expand Down Expand Up @@ -151,7 +151,7 @@ const JSONT = JSON as JSONT;
}>;
(async () => {
const f = fetch as FetchT<"", Spec>;
// TODO: getが定義されていない場合、methodを省略したらエラーになってほしいが今はならない
// @ts-expect-error getが定義されていない場合、methodは省略できない
await f(`/users`, {});
})();
}
Expand Down Expand Up @@ -208,6 +208,34 @@ const JSONT = JSON as JSONT;
})();
}

{
type Spec = DefineApiEndpoints<{
"/packages/list": {
get: {
responses: { 200: { body: { prop: string } } };
query: { state?: boolean };
};
};
}>;
(async () => {
const basePath = "/api/projects/:projectName/workflow";
const f = fetch as FetchT<typeof basePath, Spec>;
{
const res = await f(
`/api/projects/projectA/workflow/packages/list?state=true`,
{},
);
if (res.ok) {
(await res.json()).prop;
}
}
{
// query parameter can be omitted because it is optional
f(`/api/projects/projectA/workflow/packages/list`, {});
}
})();
}

{
type Spec = DefineApiEndpoints<{
"/vectorize/indexes/:indexName": {
Expand Down
43 changes: 22 additions & 21 deletions src/fetch/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
ApiEndpoints,
ApiHasP,
ApiP,
AnyApiResponses,
CaseInsensitiveMethod,
Expand All @@ -13,6 +12,7 @@ import {
Replace,
StatusCode,
IsAllOptional,
CaseInsensitive,
} from "../common";
import { UrlPrefixPattern, ToUrlParamPattern } from "../common";
import { TypedString } from "../json";
Expand All @@ -22,10 +22,12 @@ export type RequestInitT<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Body extends Record<string, any> | undefined,
HeadersObj extends Record<string, string> | undefined,
> = Omit<RequestInit, "method" | "body" | "headers"> & {
method?: InputMethod;
> = Omit<RequestInit, "method" | "body" | "headers"> &
(InputMethod extends "get" | "GET"
? { method?: InputMethod }
: { method: InputMethod }) &
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} & (Body extends Record<string, any>
(Body extends Record<string, any>
? IsAllOptional<Body> extends true
? { body?: Body | TypedString<Body> }
: { body: TypedString<Body> }
Expand All @@ -42,17 +44,24 @@ export type RequestInitT<
* FetchT is a type for window.fetch like function but more strict type information
*/
type FetchT<UrlPrefix extends UrlPrefixPattern, E extends ApiEndpoints> = <
UrlPattern extends ToUrlParamPattern<`${UrlPrefix}${keyof E & string}`>,
Input extends Query extends undefined
? ToUrlParamPattern<`${UrlPrefix}${keyof E & string}`>
: `${ToUrlParamPattern<`${UrlPrefix}${keyof E & string}`>}?${string}`,
? UrlPattern
: IsAllOptional<Query> extends true
? UrlPattern | `${UrlPattern}?${string}`
: `${UrlPattern}?${string}`,
InputPath extends PathToUrlParamPattern<
NormalizePath<
ParseURL<Replace<Input, ToUrlParamPattern<UrlPrefix>, "">>["path"]
>
>,
CandidatePaths extends string = MatchedPatterns<InputPath, keyof E & string>,
InputMethod extends CaseInsensitiveMethod = "get",
M extends Method = Lowercase<InputMethod>,
InputMethod extends CaseInsensitive<keyof E[CandidatePaths] & string> &
CaseInsensitiveMethod = CaseInsensitive<keyof E[CandidatePaths] & string> &
CaseInsensitiveMethod,
M extends Method = CaseInsensitive<"get"> extends InputMethod
? "get"
: Lowercase<InputMethod>,
Query extends ApiP<E, CandidatePaths, M, "query"> = ApiP<
E,
CandidatePaths,
Expand All @@ -76,19 +85,11 @@ type FetchT<UrlPrefix extends UrlPrefixPattern, E extends ApiEndpoints> = <
: Record<StatusCode, never>,
>(
input: Input,
init: ApiHasP<E, CandidatePaths, M> extends true
? RequestInitT<
InputMethod,
ApiP<E, CandidatePaths, M, "body">,
ApiP<E, CandidatePaths, M, "headers">
>
:
| RequestInitT<
InputMethod,
ApiP<E, CandidatePaths, M, "body">,
ApiP<E, CandidatePaths, M, "headers">
>
| undefined,
init: RequestInitT<
InputMethod,
ApiP<E, CandidatePaths, M, "body">,
ApiP<E, CandidatePaths, M, "headers">
>,
) => Promise<ResBody>;

export default FetchT;