From f2575902fc395e4cd171137e4dc5ce73c0000392 Mon Sep 17 00:00:00 2001 From: Anurag Kumar <65001074+anuragkumar19@users.noreply.github.com> Date: Tue, 5 Mar 2024 09:48:40 +0530 Subject: [PATCH] feat(types): typed api definition --- src/types.ts | 328 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 304 insertions(+), 24 deletions(-) diff --git a/src/types.ts b/src/types.ts index d4f2e11c..1ecd41df 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,27 +2,81 @@ // $fetch API // -------------------------- -export interface $Fetch { - ( - request: FetchRequest, - options?: FetchOptions - ): Promise>; - raw( - request: FetchRequest, - options?: FetchOptions - ): Promise>>; +// TODO: set default to any for backward compatibility +export interface $Fetch { + < + T = DefaultT, + ResT extends ResponseType = "json", + R extends ExtendedFetchRequest = ExtendedFetchRequest, + M extends + | ExtractedRouteMethod + | Uppercase> = + | ExtractedRouteMethod + | Uppercase>, + >( + request: R, + opts?: FetchOptions< + ResT, + { + method: M; + query: TypedInternalQuery, Lowercase>; + body: TypedInternalBody>; + params: TypedInternalParams, Lowercase>; + } + > + ): Promise< + MappedResponseType>> + >; + raw< + T = DefaultT, + ResT extends ResponseType = "json", + R extends ExtendedFetchRequest = ExtendedFetchRequest, + M extends + | ExtractedRouteMethod + | Uppercase> = + | ExtractedRouteMethod + | Uppercase>, + >( + request: R, + opts?: FetchOptions< + ResT, + { + method: M; + query: TypedInternalQuery, Lowercase>; + body: TypedInternalBody>; + params: TypedInternalParams, Lowercase>; + } + > + ): Promise< + FetchResponse< + MappedResponseType>> + > + >; + create( + defaults: FetchOptions + ): $Fetch; native: Fetch; - create(defaults: FetchOptions): $Fetch; } +// -------------------------- +// Internal API +// -------------------------- + +export interface InternalApi {} + // -------------------------- // Context // -------------------------- -export interface FetchContext { +export interface FetchContext< + T = any, + ResT extends ResponseType = ResponseType, + // eslint-disable-next-line @typescript-eslint/ban-types + O extends object = {}, +> { request: FetchRequest; // eslint-disable-next-line no-use-before-define - options: FetchOptions; + options: FetchOptions; response?: FetchResponse; error?: Error; } @@ -31,15 +85,21 @@ export interface FetchContext { // Options // -------------------------- -export interface FetchOptions - extends Omit { +export interface FetchOptions< + ResT extends ResponseType = ResponseType, + // eslint-disable-next-line @typescript-eslint/ban-types + O extends object = {}, +> extends Omit { + method?: O extends { method: infer M } ? M : RequestInit["method"]; baseURL?: string; - body?: RequestInit["body"] | Record; + body?: O extends { body: infer B } + ? B + : RequestInit["body"] | Record; ignoreResponseError?: boolean; - params?: Record; - query?: Record; + params?: O extends { params: infer P } ? P : Record; + query?: O extends { query: infer Q } ? Q : Record; parseResponse?: (responseText: string) => any; - responseType?: R; + responseType?: ResT; /** * @experimental Set to "half" to enable duplex streaming. @@ -57,15 +117,15 @@ export interface FetchOptions /** Default is [408, 409, 425, 429, 500, 502, 503, 504] */ retryStatusCodes?: number[]; - onRequest?(context: FetchContext): Promise | void; + onRequest?(context: FetchContext): Promise | void; onRequestError?( - context: FetchContext & { error: Error } + context: FetchContext & { error: Error } ): Promise | void; onResponse?( - context: FetchContext & { response: FetchResponse } + context: FetchContext & { response: FetchResponse } ): Promise | void; onResponseError?( - context: FetchContext & { response: FetchResponse } + context: FetchContext & { response: FetchResponse } ): Promise | void; } @@ -96,9 +156,9 @@ export interface ResponseMap { export type ResponseType = keyof ResponseMap | "json"; export type MappedResponseType< - R extends ResponseType, + ResT extends ResponseType, JsonType = any, -> = R extends keyof ResponseMap ? ResponseMap[R] : JsonType; +> = ResT extends keyof ResponseMap ? ResponseMap[ResT] : JsonType; export interface FetchResponse extends Response { _data?: T; @@ -127,6 +187,226 @@ export type Fetch = typeof globalThis.fetch; export type FetchRequest = RequestInfo; +export type ExtendedFetchRequest = + | keyof A + | Exclude + // eslint-disable-next-line @typescript-eslint/ban-types + | (string & {}); + export interface SearchParameters { [key: string]: any; } + +// -------------------------- +// Utility types +// -------------------------- + +export type HTTPMethod = + | "GET" + | "HEAD" + | "PATCH" + | "POST" + | "PUT" + | "DELETE" + | "CONNECT" + | "OPTIONS" + | "TRACE"; + +export type RouterMethod = Lowercase; + +// An interface to extend in a local project +export type TypedInternalResponse< + Route, + A extends object, + Default = unknown, + Method extends RouterMethod = RouterMethod, +> = Default extends string | boolean | number | null | void | object + ? // Allow user overrides + Default + : Route extends string + ? Method extends keyof A[MatchedRoutes] + ? A[MatchedRoutes][Method] extends { response: infer T } + ? [T] extends [never] + ? Default + : T + : Default + : A[MatchedRoutes]["default"] extends { response: infer T } + ? [T] extends [never] + ? Default + : T + : Default + : Default; + +export type TypedInternalQuery< + Route, + A extends object, + Default, + Method extends RouterMethod = RouterMethod, +> = Route extends string // TODO: Allow user overrides + ? Method extends keyof A[MatchedRoutes] + ? A[MatchedRoutes][Method] extends { + request: { query: infer T }; + } + ? T + : Default + : A[MatchedRoutes]["default"] extends { + request: { query: infer T }; + } + ? T + : Default + : Default; + +export type TypedInternalParams< + Route, + A extends object, + Default, + Method extends RouterMethod = RouterMethod, +> = Route extends string // TODO: Allow user overrides + ? Method extends keyof A[MatchedRoutes] + ? A[MatchedRoutes][Method] extends { + request: { params: infer T }; + } + ? T + : Default + : A[MatchedRoutes]["default"] extends { + request: { params: infer T }; + } + ? T + : Default + : Default; + +export type TypedInternalBody< + Route, + A extends object, + Default, + Method extends RouterMethod = RouterMethod, +> = Route extends string // TODO: Allow user overrides + ? Method extends keyof A[MatchedRoutes] + ? A[MatchedRoutes][Method] extends { + request: { body: infer T }; + } + ? T + : Default + : A[MatchedRoutes]["default"] extends { + request: { body: infer T }; + } + ? T + : Default + : Default; + +// Extract the route method from options which might be undefined or without a method parameter. +export type ExtractedRouteMethod< + // TODO: improvement needed + A extends object, + R extends ExtendedFetchRequest, +> = R extends string + ? keyof A[MatchedRoutes] extends RouterMethod + ? keyof A[MatchedRoutes] + : RouterMethod + : RouterMethod; + +type MatchResult< + Key extends string, + Exact extends boolean = false, + Score extends any[] = [], + catchAll extends boolean = false, +> = { + [k in Key]: { key: k; exact: Exact; score: Score; catchAll: catchAll }; +}[Key]; + +type Subtract< + Minuend extends any[] = [], + Subtrahend extends any[] = [], +> = Minuend extends [...Subtrahend, ...infer Remainder] ? Remainder : never; + +type TupleIfDiff< + First extends string, + Second extends string, + Tuple extends any[] = [], +> = First extends `${Second}${infer Diff}` + ? Diff extends "" + ? [] + : Tuple + : []; + +type MaxTuple = { + current: T; + result: MaxTuple; +}[[N["length"]] extends [Partial["length"]] ? "current" : "result"]; + +type CalcMatchScore< + Key extends string, + Route extends string, + Score extends any[] = [], + Init extends boolean = false, + FirstKeySegMatcher extends string = Init extends true ? ":Invalid:" : "", +> = `${Key}/` extends `${infer KeySeg}/${infer KeyRest}` + ? KeySeg extends FirstKeySegMatcher // return score if `KeySeg` is empty string (except first pass) + ? Subtract< + [...Score, ...TupleIfDiff], + TupleIfDiff + > + : `${Route}/` extends `${infer RouteSeg}/${infer RouteRest}` + ? `${RouteSeg}?` extends `${infer RouteSegWithoutQuery}?${string}` + ? RouteSegWithoutQuery extends KeySeg + ? CalcMatchScore // exact match + : KeySeg extends `:${string}` + ? RouteSegWithoutQuery extends "" + ? never + : CalcMatchScore // param match + : KeySeg extends RouteSegWithoutQuery + ? CalcMatchScore // match by ${string} + : never + : never + : never + : never; + +type _MatchedRoutes< + Route extends string, + A extends object, + MatchedResultUnion extends MatchResult = MatchResult< + Exclude, symbol> + >, +> = MatchedResultUnion["key"] extends infer MatchedKeys // spread union type + ? MatchedKeys extends string + ? Route extends MatchedKeys + ? MatchResult // exact match + : MatchedKeys extends `${infer Root}/**${string}` + ? MatchedKeys extends `${string}/**` + ? Route extends `${Root}/${string}` + ? MatchResult + : never // catchAll match + : MatchResult< + MatchedKeys, + false, + CalcMatchScore + > // glob match + : MatchResult< + MatchedKeys, + false, + CalcMatchScore + > // partial match + : never + : never; + +export type MatchedRoutes< + Route extends string, + A extends object, + MatchedKeysResult extends MatchResult = MatchResult< + Exclude, symbol> + >, + Matches extends MatchResult = _MatchedRoutes< + Route, + A, + MatchedKeysResult + >, +> = Route extends "/" + ? keyof A // root middleware + : Extract extends never + ? + | Extract< + Exclude, + { score: MaxTuple } + >["key"] + | Extract["key"] // partial, glob and catchAll matches + : Extract["key"]; // exact matches