-
Notifications
You must be signed in to change notification settings - Fork 2
Add IsValidUrl type #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add IsValidUrl type #114
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { Equal, Expect } from "./type-test"; | ||
| import { | ||
| HasExcessiveQuery, | ||
| HasMissingQuery, | ||
| IsValidQuery, | ||
| NonOptionalKeys, | ||
| ToQueryUnion, | ||
| } from "./query-string"; | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| type ToQueryUnionCase = [ | ||
| Expect<Equal<ToQueryUnion<"a=1">, "a">>, | ||
| Expect<Equal<ToQueryUnion<"a=1&b=2">, "a" | "b">>, | ||
| Expect<Equal<ToQueryUnion<"a=1&b=2&a=3">, "a" | "b">>, | ||
| ]; | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| type HasMissingQueryCase = [ | ||
| Expect<Equal<HasMissingQuery<{ a: string }, "a">, false>>, | ||
| Expect<Equal<HasMissingQuery<{ a?: string }, "a">, false>>, | ||
| Expect<Equal<HasMissingQuery<{ a?: string }, "b">, false>>, | ||
| Expect<Equal<HasMissingQuery<{ a?: string }, never>, false>>, | ||
| Expect<Equal<HasMissingQuery<{ a: string }, "a" | "b">, false>>, | ||
| Expect<Equal<HasMissingQuery<{ a?: string }, "a" | "b">, false>>, | ||
| Expect<Equal<HasMissingQuery<{ a: string; b?: string }, "a">, false>>, | ||
| Expect<Equal<HasMissingQuery<{ a: string }, "b">, true>>, | ||
| Expect<Equal<HasMissingQuery<{ a: string; b: string }, "b">, true>>, | ||
| Expect<Equal<HasMissingQuery<{ a: string; b?: string }, "b">, true>>, | ||
| ]; | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| type HasExcessiveQueryCase = [ | ||
| Expect<Equal<HasExcessiveQuery<{ a: string }, "a">, false>>, | ||
| Expect<Equal<HasExcessiveQuery<{ a: string }, "b">, true>>, | ||
| Expect<Equal<HasExcessiveQuery<{ a: string }, "a" | "b">, true>>, | ||
| Expect<Equal<HasExcessiveQuery<{ a: string; b: string }, "a" | "b">, false>>, | ||
| Expect<Equal<HasExcessiveQuery<{ a: string; b?: string }, "a" | "b">, false>>, | ||
| Expect< | ||
| Equal<HasExcessiveQuery<{ a: string; b: string }, "a" | "b" | "c">, true> | ||
| >, | ||
| ]; | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| type NonOptionalKeysCase = [ | ||
| Expect<Equal<NonOptionalKeys<{ a: string }>, "a">>, | ||
| Expect<Equal<NonOptionalKeys<{ a?: string }>, never>>, | ||
| Expect<Equal<NonOptionalKeys<{ a: string; b?: string }>, "a">>, | ||
| Expect<Equal<NonOptionalKeys<{ a: string; b: string }>, "a" | "b">>, | ||
| ]; | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| type IsValidQueryCase = [ | ||
| Expect<Equal<IsValidQuery<{ a: string }, "a">, true>>, | ||
| Expect<Equal<IsValidQuery<{ a: string }, "b">, "E: maybe missing query: a">>, | ||
| Expect< | ||
| Equal< | ||
| IsValidQuery<{ a: string }, "a" | "b">, | ||
| "E: maybe excessive query: a" | "E: maybe excessive query: b" | ||
| > | ||
| >, | ||
| Expect<Equal<IsValidQuery<{ a: string; b?: string }, "a">, true>>, | ||
| Expect<Equal<IsValidQuery<{ a: string; b?: string }, "a" | "b">, true>>, | ||
| Expect< | ||
| Equal< | ||
| IsValidQuery<{ a: string; b: string }, "a">, | ||
| "E: maybe missing query: a" | "E: maybe missing query: b" | ||
| > | ||
| >, | ||
| Expect<Equal<IsValidQuery<{ a: string; b: string }, "a" | "b">, true>>, | ||
| ]; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -38,3 +38,40 @@ export type SetProperty<T, K extends PropertyKey, V = true> = { | |||||||||||||||||
| ? T[P] | ||||||||||||||||||
| : never; | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| export type ExtractQuery<URL extends string> = | ||||||||||||||||||
| URL extends `${string}?${infer Query}` ? Query : undefined; | ||||||||||||||||||
|
|
||||||||||||||||||
| export type ToQueryUnion<Query extends string> = | ||||||||||||||||||
| Query extends `${infer Key}=${string}&${infer Rest}` | ||||||||||||||||||
| ? Key | ToQueryUnion<Rest> | ||||||||||||||||||
| : Query extends `${infer Key}=${string}` | ||||||||||||||||||
| ? Key | ||||||||||||||||||
| : `invalid query: ${Query}`; | ||||||||||||||||||
|
|
||||||||||||||||||
| export type HasMissingQuery< | ||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||||||||||||||
| QueryDef extends Record<string, any>, | ||||||||||||||||||
| QueryKeys extends string, | ||||||||||||||||||
| > = NonOptionalKeys<QueryDef> extends QueryKeys ? false : true; | ||||||||||||||||||
|
Comment on lines
+53
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid using Using Suggested change: - QueryDef extends Record<string, any>,
+ QueryDef extends Record<string, unknown>,Alternatively, define a more precise type for 📝 Committable suggestion
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| export type HasExcessiveQuery< | ||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||||||||||||||
| QueryDef extends Record<string, any>, | ||||||||||||||||||
| QueryKeys extends string, | ||||||||||||||||||
| // no union distribution | ||||||||||||||||||
|
Comment on lines
+59
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid using Using Suggested change: - QueryDef extends Record<string, any>,
+ QueryDef extends Record<string, unknown>,📝 Committable suggestion
Suggested change
|
||||||||||||||||||
| > = [QueryKeys] extends [keyof QueryDef] ? false : true; | ||||||||||||||||||
|
|
||||||||||||||||||
| export type NonOptionalKeys<T> = { | ||||||||||||||||||
| [K in keyof T]-?: undefined extends T[K] ? never : K; | ||||||||||||||||||
| }[keyof T]; | ||||||||||||||||||
|
|
||||||||||||||||||
| export type IsValidQuery< | ||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||||||||||||||
| QueryDef extends Record<string, any>, | ||||||||||||||||||
| QueryKeys extends string, | ||||||||||||||||||
| > = [HasMissingQuery<QueryDef, QueryKeys>] extends [true] | ||||||||||||||||||
| ? `E: maybe missing query: ${keyof QueryDef & string}` | ||||||||||||||||||
| : [HasExcessiveQuery<QueryDef, QueryKeys>] extends [true] | ||||||||||||||||||
| ? `E: maybe excessive query: ${QueryKeys}` | ||||||||||||||||||
| : true; | ||||||||||||||||||
|
Comment on lines
+69
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance error messages in The current error messages in Consider modifying the implementation to specify the exact keys that are missing or excessive to improve developer experience. Possible approach: Implement utility types to compute the specific missing or excessive keys: type MissingKeys<QueryDef, QueryKeys> = Exclude<NonOptionalKeys<QueryDef>, QueryKeys>;
type ExcessiveKeys<QueryDef, QueryKeys> = Exclude<QueryKeys, keyof QueryDef>;Then adjust export type IsValidQuery<
QueryDef extends Record<string, unknown>,
QueryKeys extends string,
> = MissingKeys<QueryDef, QueryKeys> extends never
? ExcessiveKeys<QueryDef, QueryKeys> extends never
? true
: `E: excessive query keys: ${ExcessiveKeys<QueryDef, QueryKeys>}`
: `E: missing query keys: ${MissingKeys<QueryDef, QueryKeys>}`; |
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,21 +13,34 @@ import { | |||||||||||||||||||||||||||||||||||||||||
| StatusCode, | ||||||||||||||||||||||||||||||||||||||||||
| IsAllOptional, | ||||||||||||||||||||||||||||||||||||||||||
| CaseInsensitive, | ||||||||||||||||||||||||||||||||||||||||||
| ExtractQuery, | ||||||||||||||||||||||||||||||||||||||||||
| IsValidQuery, | ||||||||||||||||||||||||||||||||||||||||||
| ToQueryUnion, | ||||||||||||||||||||||||||||||||||||||||||
| } from "../core"; | ||||||||||||||||||||||||||||||||||||||||||
| import { UrlPrefixPattern, ToUrlParamPattern } from "../core"; | ||||||||||||||||||||||||||||||||||||||||||
| import { TypedString } from "../json"; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| type IsValidUrl< | ||||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||||||||||||||||||||||||||||||||||||||
| QueryDef extends Record<string, unknown> | undefined, | ||||||||||||||||||||||||||||||||||||||||||
| Url extends string, | ||||||||||||||||||||||||||||||||||||||||||
| Query extends string | undefined = ExtractQuery<Url>, | ||||||||||||||||||||||||||||||||||||||||||
| QueryKeys extends string = Query extends string ? ToQueryUnion<Query> : never, | ||||||||||||||||||||||||||||||||||||||||||
| > = IsValidQuery< | ||||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types | ||||||||||||||||||||||||||||||||||||||||||
| QueryDef extends Record<string, any> ? QueryDef : {}, | ||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use 'Record<string, never>' instead of '{}' as a type Using Apply this diff to fix the issue: - QueryDef extends Record<string, any> ? QueryDef : {},
+ QueryDef extends Record<string, any> ? QueryDef : Record<string, never>,📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome
|
||||||||||||||||||||||||||||||||||||||||||
| QueryKeys | ||||||||||||||||||||||||||||||||||||||||||
| >; | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improve type safety and remove ESLint disable comments. The
Apply this diff to improve type safety: type IsValidUrl<
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
QueryDef extends Record<string, unknown> | undefined,
Url extends string,
Query extends string | undefined = ExtractQuery<Url>,
QueryKeys extends string = Query extends string ? ToQueryUnion<Query> : never,
> = IsValidQuery<
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
- QueryDef extends Record<string, any> ? QueryDef : {},
+ QueryDef extends Record<string, unknown> ? QueryDef : Record<string, never>,
QueryKeys
>;This change removes the need for ESLint disable comments and improves type safety by using 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export type RequestInitT< | ||||||||||||||||||||||||||||||||||||||||||
| InputMethod extends CaseInsensitiveMethod, | ||||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||||||||||||||||||||||||||||||||||||||
| Body extends Record<string, any> | string | undefined, | ||||||||||||||||||||||||||||||||||||||||||
| Body extends Record<string, unknown> | string | undefined, | ||||||||||||||||||||||||||||||||||||||||||
| HeadersObj extends string | Record<string, string> | undefined, | ||||||||||||||||||||||||||||||||||||||||||
| > = 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, unknown> | ||||||||||||||||||||||||||||||||||||||||||
| ? IsAllOptional<Body> extends true | ||||||||||||||||||||||||||||||||||||||||||
| ? { body?: Body | TypedString<Body> } | ||||||||||||||||||||||||||||||||||||||||||
| : { body: TypedString<Body> } | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -86,7 +99,7 @@ type FetchT<UrlPrefix extends UrlPrefixPattern, E extends ApiEndpoints> = < | |||||||||||||||||||||||||||||||||||||||||
| ? MergeApiResponseBodies<ApiP<E, CandidatePaths, M, "responses">> | ||||||||||||||||||||||||||||||||||||||||||
| : Record<StatusCode, never>, | ||||||||||||||||||||||||||||||||||||||||||
| >( | ||||||||||||||||||||||||||||||||||||||||||
| input: Input, | ||||||||||||||||||||||||||||||||||||||||||
| input: IsValidUrl<Query, Input> extends true ? Input : never, | ||||||||||||||||||||||||||||||||||||||||||
| init: RequestInitT< | ||||||||||||||||||||||||||||||||||||||||||
| InputMethod, | ||||||||||||||||||||||||||||||||||||||||||
| ApiP<E, CandidatePaths, M, "body">, | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider refining the error handling in
ToQueryUnionCurrently, if the query string does not match the expected patterns, the
ToQueryUniontype returns a string literal of the formatinvalid query: ${Query}. This may complicate type comparisons and error handling.Consider returning
neveror a custom error type to improve type safety and clarity.Suggested change:
export type ToQueryUnion<Query extends string> = Query extends `${infer Key}=${string}&${infer Rest}` ? Key | ToQueryUnion<Rest> : Query extends `${infer Key}=${string}` ? Key - : `invalid query: ${Query}`; + : never;📝 Committable suggestion