-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
Enhance client data type inference #8269
Changes from all commits
7842037
1370c68
c5a8312
70cfc29
8febabb
dcd9032
9a0c480
6ec8736
ed3a0f4
15cc737
b84bf03
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,5 @@ | ||
--- | ||
"@remix-run/react": patch | ||
--- | ||
|
||
[REMOVE] Enhance inferred types to avoid deserialization for client data functions |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,60 @@ | ||
import type { Jsonify } from "./jsonify"; | ||
import type { EmptyObject, Jsonify } from "./jsonify"; | ||
import type { TypedDeferredData, TypedResponse } from "./responses"; | ||
import type { | ||
ClientActionFunctionArgs, | ||
ClientLoaderFunctionArgs, | ||
} from "./routeModules"; | ||
import { expectType } from "./typecheck"; | ||
import { type Expect, type Equal } from "./typecheck"; | ||
|
||
// prettier-ignore | ||
/** | ||
* Infer JSON serialized data type returned by a loader or action. | ||
* Infer JSON serialized data type returned by a loader or action, while | ||
* avoiding deserialization if the input type if it's a clientLoader or | ||
* clientAction that returns a non-Response | ||
* | ||
* For example: | ||
* `type LoaderData = SerializeFrom<typeof loader>` | ||
*/ | ||
export type SerializeFrom<T> = | ||
T extends (...args: any[]) => infer Output ? Serialize<Awaited<Output>> : | ||
T extends (...args: any[]) => infer Output ? | ||
Parameters<T> extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] ? | ||
// Client data functions may not serialize | ||
SerializeClient<Awaited<Output>> | ||
: | ||
Comment on lines
+21
to
+24
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. Cleaned it up so that this is all that's new and we don't touch the flow for existing loaders/actions |
||
// Serialize responses | ||
Serialize<Awaited<Output>> | ||
: | ||
// Back compat: manually defined data type, not inferred from loader nor action | ||
Jsonify<Awaited<T>> | ||
; | ||
|
||
// note: cannot be inlined as logic requires union distribution | ||
// prettier-ignore | ||
type SerializeClient<Output> = | ||
Output extends TypedDeferredData<infer U> ? | ||
// top-level promises | ||
& { | ||
[K in keyof U as K extends symbol | ||
? never | ||
: Promise<any> extends U[K] | ||
? K | ||
: never]: DeferValueClient<U[K]>; // use generic to distribute over union | ||
} | ||
// non-promises | ||
& { | ||
[K in keyof U as Promise<any> extends U[K] ? never : K]: U[K]; | ||
} | ||
: | ||
Output extends TypedResponse<infer U> ? Jsonify<U> : | ||
Awaited<Output> | ||
|
||
// prettier-ignore | ||
type DeferValueClient<T> = | ||
T extends undefined ? undefined : | ||
T extends Promise<unknown> ? Promise<Awaited<T>> : | ||
T; | ||
|
||
Comment on lines
+32
to
+57
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. Copies of the |
||
// note: cannot be inlined as logic requires union distribution | ||
// prettier-ignore | ||
type Serialize<Output> = | ||
|
@@ -49,16 +88,45 @@ type DeferValue<T> = | |
|
||
type Pretty<T> = { [K in keyof T]: T[K] }; | ||
|
||
type Loader<T> = () => Promise< | ||
| TypedResponse<T> // returned responses | ||
| TypedResponse<never> // thrown responses | ||
>; | ||
type Loader<T> = () => Promise<TypedResponse<T>>; | ||
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. @pcattori and I worked through some examples and we don't need to represent the never types since they don't show up in the signature if you throw conditionally. |
||
|
||
type LoaderDefer<T extends Record<keyof unknown, unknown>> = () => Promise< | ||
| TypedDeferredData<T> // returned responses | ||
| TypedResponse<never> // thrown responses | ||
TypedDeferredData<T> | ||
>; | ||
|
||
type LoaderBoth< | ||
T1 extends Record<keyof unknown, unknown>, | ||
T2 extends Record<keyof unknown, unknown> | ||
> = () => Promise<TypedResponse<T1> | TypedDeferredData<T2>>; | ||
|
||
type ClientLoaderRaw<T extends Record<keyof unknown, unknown>> = ({ | ||
request, | ||
}: ClientLoaderFunctionArgs) => Promise<T>; // returned non-Response | ||
|
||
type ClientLoaderResponse<T extends Record<keyof unknown, unknown>> = ({ | ||
request, | ||
}: ClientLoaderFunctionArgs) => Promise<TypedResponse<T>>; // returned responses | ||
|
||
type ClientLoaderDefer<T extends Record<keyof unknown, unknown>> = ({ | ||
request, | ||
}: ClientLoaderFunctionArgs) => Promise<TypedDeferredData<T>>; // returned responses | ||
|
||
type ClientLoaderResponseAndDefer< | ||
T1 extends Record<keyof unknown, unknown>, | ||
T2 extends Record<keyof unknown, unknown> | ||
> = ({ | ||
request, | ||
}: ClientLoaderFunctionArgs) => Promise< | ||
TypedResponse<T1> | TypedDeferredData<T2> | ||
>; | ||
|
||
type ClientLoaderRawAndDefer< | ||
T1 extends Record<keyof unknown, unknown>, | ||
T2 extends Record<keyof unknown, unknown> | ||
> = ({ | ||
request, | ||
}: ClientLoaderFunctionArgs) => Promise<T1 | TypedDeferredData<T2>>; | ||
|
||
// prettier-ignore | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
type _tests = [ | ||
|
@@ -78,7 +146,27 @@ type _tests = [ | |
Expect<Equal<Pretty<SerializeFrom<Loader<{a: string, name: number, data: boolean}>>>, {a: string, name: number, data: boolean}>>, | ||
|
||
// defer top-level promises | ||
Expect<SerializeFrom<LoaderDefer<{ a: string; lazy: Promise<{ b: number }>}>> extends {a: string, lazy: Promise<{ b: number }>} ? true : false> | ||
Expect<SerializeFrom<LoaderDefer<{ a: string; lazy: Promise<{ b: number }>}>> extends {a: string, lazy: Promise<{ b: number }>} ? true : false>, | ||
|
||
// conditional defer or json | ||
Expect<SerializeFrom<LoaderBoth<{ a:string, b: Promise<string> }, { c: string; lazy: Promise<{ d: number }>}>> extends { a: string, b: EmptyObject } | { c: string; lazy: Promise<{ d: number }> } ? true : false>, | ||
|
||
// clientLoader raw JSON | ||
Expect<Equal<Pretty<SerializeFrom<ClientLoaderRaw<{a: string}>>>, {a: string}>>, | ||
Expect<Equal<Pretty<SerializeFrom<ClientLoaderRaw<{a: Date, b: Map<string,number> }>>>, {a: Date, b: Map<string,number>}>>, | ||
|
||
// clientLoader json() Response | ||
Expect<Equal<Pretty<SerializeFrom<ClientLoaderResponse<{a: string}>>>, {a: string}>>, | ||
Expect<Equal<Pretty<SerializeFrom<ClientLoaderResponse<{a: Date}>>>, {a: string}>>, | ||
|
||
// clientLoader defer() data | ||
Expect<SerializeFrom<ClientLoaderDefer<{ a: string; lazy: Promise<{ b: number }>}>> extends {a: string, lazy: Promise<{ b: number }>} ? true : false>, | ||
|
||
// clientLoader conditional defer or json | ||
Expect<SerializeFrom<ClientLoaderResponseAndDefer<{ a: string, b: Promise<string> }, { c: string; lazy: Promise<{ d: number }>}>> extends { a: string, b: EmptyObject } | { c: string; lazy: Promise<{ d: number }> } ? true : false>, | ||
|
||
// clientLoader conditional defer or raw | ||
Expect<SerializeFrom<ClientLoaderRawAndDefer<{ a: string, b: Promise<string> }, { c: string; lazy: Promise<{ d: number }>}>> extends { a: string, b: Promise<string> } | { c: string; lazy: Promise<{ d: number }> } ? true : false>, | ||
]; | ||
|
||
// recursive | ||
|
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.