Skip to content
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

Merged
merged 11 commits into from
Dec 13, 2023
Merged
5 changes: 5 additions & 0 deletions .changeset/clean-boats-deliver.md
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
6 changes: 5 additions & 1 deletion packages/remix-server-runtime/jsonify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export type Jsonify<T> =
T extends Number ? number :
T extends Boolean ? boolean :

// Promises JSON.stringify to an empty object
T extends Promise<unknown> ? EmptyObject :
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2023-12-13 at 3 26 19 PM


// Map & Set
T extends Map<unknown, unknown> ? EmptyObject :
T extends Set<unknown> ? EmptyObject :
Expand Down Expand Up @@ -119,6 +122,7 @@ type _tests = [
Expect<Equal<Jsonify<String>, string>>,
Expect<Equal<Jsonify<Number>, number>>,
Expect<Equal<Jsonify<Boolean>, boolean>>,
Expect<Equal<Jsonify<Promise<string>>, EmptyObject>>,

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

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

// adapted from https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts
type IsAny<T> = 0 extends 1 & T ? true : false;
4 changes: 2 additions & 2 deletions packages/remix-server-runtime/routeModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ type ClientActionFunction = (
* Arguments passed to a route `clientAction` function
* @private Public API is exported from @remix-run/react
*/
type ClientActionFunctionArgs = RRActionFunctionArgs<undefined> & {
export type ClientActionFunctionArgs = RRActionFunctionArgs<undefined> & {
serverAction: <T = AppData>() => Promise<SerializeFrom<T>>;
};

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

Expand Down
108 changes: 98 additions & 10 deletions packages/remix-server-runtime/serialize.ts
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copies of the Serialize/DeferValue types with Jsonify stuff removed (unless it's a TypedResponse and then we still call Jsonify)

// note: cannot be inlined as logic requires union distribution
// prettier-ignore
type Serialize<Output> =
Expand Down Expand Up @@ -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>>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 = [
Expand All @@ -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
Expand Down