Skip to content

Commit dabccfe

Browse files
authored
Enhance client data type inference (#8269)
1 parent a23ecb6 commit dabccfe

File tree

3 files changed

+105
-13
lines changed

3 files changed

+105
-13
lines changed

packages/remix-server-runtime/jsonify.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export type Jsonify<T> =
2121
T extends Number ? number :
2222
T extends Boolean ? boolean :
2323

24+
// Promises JSON.stringify to an empty object
25+
T extends Promise<unknown> ? EmptyObject :
26+
2427
// Map & Set
2528
T extends Map<unknown, unknown> ? EmptyObject :
2629
T extends Set<unknown> ? EmptyObject :
@@ -119,6 +122,7 @@ type _tests = [
119122
Expect<Equal<Jsonify<String>, string>>,
120123
Expect<Equal<Jsonify<Number>, number>>,
121124
Expect<Equal<Jsonify<Boolean>, boolean>>,
125+
Expect<Equal<Jsonify<Promise<string>>, EmptyObject>>,
122126

123127
// Map & Set
124128
Expect<Equal<Jsonify<Map<unknown, unknown>>, EmptyObject>>,
@@ -251,7 +255,7 @@ type NeverToNull<T> = [T] extends [never] ? null : T;
251255

252256
// adapted from https://github.com/sindresorhus/type-fest/blob/main/source/empty-object.d.ts
253257
declare const emptyObjectSymbol: unique symbol;
254-
type EmptyObject = { [emptyObjectSymbol]?: never };
258+
export type EmptyObject = { [emptyObjectSymbol]?: never };
255259

256260
// adapted from https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts
257261
type IsAny<T> = 0 extends 1 & T ? true : false;

packages/remix-server-runtime/routeModules.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ type ClientActionFunction = (
5454
* Arguments passed to a route `clientAction` function
5555
* @private Public API is exported from @remix-run/react
5656
*/
57-
type ClientActionFunctionArgs = RRActionFunctionArgs<undefined> & {
57+
export type ClientActionFunctionArgs = RRActionFunctionArgs<undefined> & {
5858
serverAction: <T = AppData>() => Promise<SerializeFrom<T>>;
5959
};
6060

@@ -87,7 +87,7 @@ type ClientLoaderFunction = ((
8787
* Arguments passed to a route `clientLoader` function
8888
* @private Public API is exported from @remix-run/react
8989
*/
90-
type ClientLoaderFunctionArgs = RRLoaderFunctionArgs<undefined> & {
90+
export type ClientLoaderFunctionArgs = RRLoaderFunctionArgs<undefined> & {
9191
serverLoader: <T = AppData>() => Promise<SerializeFrom<T>>;
9292
};
9393

packages/remix-server-runtime/serialize.ts

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,60 @@
1-
import type { Jsonify } from "./jsonify";
1+
import type { EmptyObject, Jsonify } from "./jsonify";
22
import type { TypedDeferredData, TypedResponse } from "./responses";
3+
import type {
4+
ClientActionFunctionArgs,
5+
ClientLoaderFunctionArgs,
6+
} from "./routeModules";
37
import { expectType } from "./typecheck";
48
import { type Expect, type Equal } from "./typecheck";
59

610
// prettier-ignore
711
/**
8-
* Infer JSON serialized data type returned by a loader or action.
12+
* Infer JSON serialized data type returned by a loader or action, while
13+
* avoiding deserialization if the input type if it's a clientLoader or
14+
* clientAction that returns a non-Response
915
*
1016
* For example:
1117
* `type LoaderData = SerializeFrom<typeof loader>`
1218
*/
1319
export type SerializeFrom<T> =
14-
T extends (...args: any[]) => infer Output ? Serialize<Awaited<Output>> :
20+
T extends (...args: any[]) => infer Output ?
21+
Parameters<T> extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] ?
22+
// Client data functions may not serialize
23+
SerializeClient<Awaited<Output>>
24+
:
25+
// Serialize responses
26+
Serialize<Awaited<Output>>
27+
:
1528
// Back compat: manually defined data type, not inferred from loader nor action
1629
Jsonify<Awaited<T>>
1730
;
1831

32+
// note: cannot be inlined as logic requires union distribution
33+
// prettier-ignore
34+
type SerializeClient<Output> =
35+
Output extends TypedDeferredData<infer U> ?
36+
// top-level promises
37+
& {
38+
[K in keyof U as K extends symbol
39+
? never
40+
: Promise<any> extends U[K]
41+
? K
42+
: never]: DeferValueClient<U[K]>; // use generic to distribute over union
43+
}
44+
// non-promises
45+
& {
46+
[K in keyof U as Promise<any> extends U[K] ? never : K]: U[K];
47+
}
48+
:
49+
Output extends TypedResponse<infer U> ? Jsonify<U> :
50+
Awaited<Output>
51+
52+
// prettier-ignore
53+
type DeferValueClient<T> =
54+
T extends undefined ? undefined :
55+
T extends Promise<unknown> ? Promise<Awaited<T>> :
56+
T;
57+
1958
// note: cannot be inlined as logic requires union distribution
2059
// prettier-ignore
2160
type Serialize<Output> =
@@ -49,16 +88,45 @@ type DeferValue<T> =
4988

5089
type Pretty<T> = { [K in keyof T]: T[K] };
5190

52-
type Loader<T> = () => Promise<
53-
| TypedResponse<T> // returned responses
54-
| TypedResponse<never> // thrown responses
55-
>;
91+
type Loader<T> = () => Promise<TypedResponse<T>>;
5692

5793
type LoaderDefer<T extends Record<keyof unknown, unknown>> = () => Promise<
58-
| TypedDeferredData<T> // returned responses
59-
| TypedResponse<never> // thrown responses
94+
TypedDeferredData<T>
95+
>;
96+
97+
type LoaderBoth<
98+
T1 extends Record<keyof unknown, unknown>,
99+
T2 extends Record<keyof unknown, unknown>
100+
> = () => Promise<TypedResponse<T1> | TypedDeferredData<T2>>;
101+
102+
type ClientLoaderRaw<T extends Record<keyof unknown, unknown>> = ({
103+
request,
104+
}: ClientLoaderFunctionArgs) => Promise<T>; // returned non-Response
105+
106+
type ClientLoaderResponse<T extends Record<keyof unknown, unknown>> = ({
107+
request,
108+
}: ClientLoaderFunctionArgs) => Promise<TypedResponse<T>>; // returned responses
109+
110+
type ClientLoaderDefer<T extends Record<keyof unknown, unknown>> = ({
111+
request,
112+
}: ClientLoaderFunctionArgs) => Promise<TypedDeferredData<T>>; // returned responses
113+
114+
type ClientLoaderResponseAndDefer<
115+
T1 extends Record<keyof unknown, unknown>,
116+
T2 extends Record<keyof unknown, unknown>
117+
> = ({
118+
request,
119+
}: ClientLoaderFunctionArgs) => Promise<
120+
TypedResponse<T1> | TypedDeferredData<T2>
60121
>;
61122

123+
type ClientLoaderRawAndDefer<
124+
T1 extends Record<keyof unknown, unknown>,
125+
T2 extends Record<keyof unknown, unknown>
126+
> = ({
127+
request,
128+
}: ClientLoaderFunctionArgs) => Promise<T1 | TypedDeferredData<T2>>;
129+
62130
// prettier-ignore
63131
// eslint-disable-next-line @typescript-eslint/no-unused-vars
64132
type _tests = [
@@ -78,7 +146,27 @@ type _tests = [
78146
Expect<Equal<Pretty<SerializeFrom<Loader<{a: string, name: number, data: boolean}>>>, {a: string, name: number, data: boolean}>>,
79147

80148
// defer top-level promises
81-
Expect<SerializeFrom<LoaderDefer<{ a: string; lazy: Promise<{ b: number }>}>> extends {a: string, lazy: Promise<{ b: number }>} ? true : false>
149+
Expect<SerializeFrom<LoaderDefer<{ a: string; lazy: Promise<{ b: number }>}>> extends {a: string, lazy: Promise<{ b: number }>} ? true : false>,
150+
151+
// conditional defer or json
152+
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>,
153+
154+
// clientLoader raw JSON
155+
Expect<Equal<Pretty<SerializeFrom<ClientLoaderRaw<{a: string}>>>, {a: string}>>,
156+
Expect<Equal<Pretty<SerializeFrom<ClientLoaderRaw<{a: Date, b: Map<string,number> }>>>, {a: Date, b: Map<string,number>}>>,
157+
158+
// clientLoader json() Response
159+
Expect<Equal<Pretty<SerializeFrom<ClientLoaderResponse<{a: string}>>>, {a: string}>>,
160+
Expect<Equal<Pretty<SerializeFrom<ClientLoaderResponse<{a: Date}>>>, {a: string}>>,
161+
162+
// clientLoader defer() data
163+
Expect<SerializeFrom<ClientLoaderDefer<{ a: string; lazy: Promise<{ b: number }>}>> extends {a: string, lazy: Promise<{ b: number }>} ? true : false>,
164+
165+
// clientLoader conditional defer or json
166+
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>,
167+
168+
// clientLoader conditional defer or raw
169+
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>,
82170
];
83171

84172
// recursive

0 commit comments

Comments
 (0)