From 2d41249813a7c45caff80cbc4f7c8077610414e5 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Tue, 10 Oct 2023 11:23:37 -0400 Subject: [PATCH 1/9] remove incorrect test case defer typings require `defer()` helper and do not support plain objects returned by loaders --- packages/remix-react/__tests__/hook-types-test.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/remix-react/__tests__/hook-types-test.tsx b/packages/remix-react/__tests__/hook-types-test.tsx index 28ffd592f00..48b2e7cdd2c 100644 --- a/packages/remix-react/__tests__/hook-types-test.tsx +++ b/packages/remix-react/__tests__/hook-types-test.tsx @@ -6,7 +6,7 @@ import type { import type { useLoaderData, useRouteLoaderData } from "../components"; function isEqual( - arg: A extends B ? (B extends A ? true : false) : false + _arg: A extends B ? (B extends A ? true : false) : false ): void {} type LoaderData = ReturnType>; @@ -322,10 +322,4 @@ describe("deferred type serializer", () => { >(true); isEqual(true); }); - - it("allows data key in value", () => { - type AppData = { data: Promise<{ hello: string }> }; - type response = LoaderData; - isEqual }>(true); - }); }); From c02c950ca6c35c36c2d0182d294609439a4b9926 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Fri, 6 Oct 2023 23:33:51 -0400 Subject: [PATCH 2/9] reimplement jsonify util --- packages/remix-server-runtime/jsonify.ts | 247 +++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 packages/remix-server-runtime/jsonify.ts diff --git a/packages/remix-server-runtime/jsonify.ts b/packages/remix-server-runtime/jsonify.ts new file mode 100644 index 00000000000..baf75deb551 --- /dev/null +++ b/packages/remix-server-runtime/jsonify.ts @@ -0,0 +1,247 @@ +// prettier-ignore +// `Jsonify` emulates `let y = JSON.parse(JSON.stringify(x))`, but for types +// so that we can infer the shape of the data sent over the network. +export type Jsonify = + // any + IsAny extends true ? any : + + // primitives + T extends JsonPrimitive ? T : + T extends String ? string : + T extends Number ? number : + T extends Boolean ? boolean : + + // Map & Set + T extends Map ? EmptyObject : + T extends Set ? EmptyObject : + + // TypedArray + T extends TypedArray ? Record : + + // Not JSON serializable + T extends NotJson ? never : + + // toJSON + T extends { toJSON(): infer U } ? (U extends JsonValue ? U : unknown) : + + // tuple & array + T extends [] ? [] : + T extends readonly [infer F, ...infer R] ? [NeverToNull>, ...Jsonify] : + T extends readonly unknown[] ? Array>>: + + // object + T extends Record ? JsonifyObject : + + // unknown + unknown extends T ? unknown : + + never + +// value is always not JSON => true +// value is always JSON => false +// value is somtimes JSON, sometimes not JSON => boolean +// note: cannot be inlined as logic requires union distribution +type ValueIsNotJson = T extends NotJson ? true : false; + +// note: remove optionality so that produced values are never `undefined`, +// only `true`, `false`, or `boolean` +type IsNotJson = { [K in keyof T]-?: ValueIsNotJson }; + +type JsonifyValues = { [K in keyof T]: Jsonify }; + +// prettier-ignore +type JsonifyObject> = + // required + { [K in keyof T as + unknown extends T[K] ? never : + IsNotJson[K] extends false ? K : + never + ]: JsonifyValues[K] } & + // optional + { [K in keyof T as + unknown extends T[K] ? K : + // if the value is always JSON, then it's not optional + IsNotJson[K] extends false ? never : + // if the value is always not JSON, omit it entirely + IsNotJson[K] extends true ? never : + // if the value is mixed, then it's optional + K + ]? : JsonifyValues[K]} + +// types ------------------------------------------------------------ + +type JsonPrimitive = string | number | boolean | null; + +type JsonArray = JsonValue[] | readonly JsonValue[]; + +// prettier-ignore +type JsonObject = + { [K in string]: JsonValue } & + { [K in string]?: JsonValue } + +type JsonValue = JsonPrimitive | JsonObject | JsonArray; + +type NotJson = undefined | symbol | AnyFunction; + +type TypedArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + | BigInt64Array + | BigUint64Array; + +// tests ------------------------------------------------------------ + +// prettier-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type _tests = [ + // any + Expect, any>>, + + // primitives + Expect, string>>, + Expect, number>>, + Expect, boolean>>, + Expect, null>>, + Expect, string>>, + Expect, number>>, + Expect, boolean>>, + + // Map & Set + Expect>, EmptyObject>>, + Expect>, EmptyObject>>, + + // TypedArray + Expect, Record>>, + Expect, Record>>, + Expect, Record>>, + Expect, Record>>, + Expect, Record>>, + Expect, Record>>, + Expect, Record>>, + Expect, Record>>, + Expect, Record>>, + Expect, Record>>, + Expect, Record>>, + + // Not Json + Expect, never>>, + Expect, never>>, + Expect void>, never>>, + Expect, never>>, + + // toJson + Expect, "stuff">>, + Expect, string>>, + Expect, unknown>>, + Expect, unknown>>, + + // tuple & array + Expect, []>>, + Expect, [1, 'two', string, null, false]>>, + Expect, (string | number)[]>>, + Expect, null[]>>, + Expect, [1,2,3]>>, + + // object + Expect>, {}>>, + Expect>, {a: string}>>, + Expect>, {a?: string}>>, + Expect>, {a?: string}>>, + Expect>, {a: string, b?: string}>>, + Expect>, {}>>, + Expect>>, Record>>, + Expect>>, Record>>, + Expect}>, { payload: Record}>>, + Expect any); + optionalFunctionUnion?: string | (() => any); + optionalFunctionUnionUndefined: string | (() => any) | undefined; + + // Should be omitted + requiredFunction: () => any; + optionalFunction?: () => any; + optionalFunctionUndefined: (() => any) | undefined; + }>>, { + requiredString: string + requiredUnion: number | boolean + + optionalString?: string; + optionalUnion?: number | string; + optionalStringUndefined?: string | undefined; + optionalUnionUndefined?: number | string | undefined; + requiredFunctionUnion?: string + optionalFunctionUnion?: string; + optionalFunctionUnionUndefined?: string + }>>, + + // unknown + Expect, unknown>>, + Expect, unknown[]>>, + Expect, [unknown, 1]>>, + Expect>, {a?: unknown}>>, + Expect>, {a?: unknown, b: 'hello'}>>, + + // never + Expect, never>>, + Expect>, {a: never}>>, + Expect>, {a: never, b:string}>>, + Expect>, {a: never, b: string} | {a: string, b: never}>>, +]; + +type Recursive = { + a: Date; + recur?: Recursive; +}; +declare const recursive: Jsonify; +expectType<{ a: string; recur?: Jsonify }>( + recursive.recur!.recur!.recur! +); + +// testing utils ---------------------------------------------------- + +// typecheck that expression is assignable to type +function expectType(_expression: T) {} + +// prettier-ignore +// adapted from https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts +type Equal = + (() => T extends X ? 1 : 2) extends + (() => T extends Y ? 1 : 2) ? true : false + +// adapted from https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts +type Expect = T; + +// looser, lazy equality check for recursive types +// prettier-ignore +type MutualExtends = [A] extends [B] ? [B] extends [A] ? true : false : false + +// general utils ---------------------------------------------------- + +type Pretty = { [K in keyof T]: T[K] }; + +type AnyFunction = (...args: any[]) => unknown; + +type NeverToNull = [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 }; + +// adapted from https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts +type IsAny = 0 extends 1 & T ? true : false; From b34f8e3061607e3ef4bfe635b7ef77c9b85be93d Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Tue, 10 Oct 2023 12:31:20 -0400 Subject: [PATCH 3/9] use new jsonify implementation in serializefrom --- .../remix-react/__tests__/hook-types-test.tsx | 2 + packages/remix-server-runtime/jsonify.ts | 27 ++---- packages/remix-server-runtime/serialize.ts | 93 ++++++++++++++----- packages/remix-server-runtime/typecheck.ts | 15 +++ 4 files changed, 94 insertions(+), 43 deletions(-) create mode 100644 packages/remix-server-runtime/typecheck.ts diff --git a/packages/remix-react/__tests__/hook-types-test.tsx b/packages/remix-react/__tests__/hook-types-test.tsx index 48b2e7cdd2c..229f3c65f5d 100644 --- a/packages/remix-react/__tests__/hook-types-test.tsx +++ b/packages/remix-react/__tests__/hook-types-test.tsx @@ -9,6 +9,8 @@ function isEqual( _arg: A extends B ? (B extends A ? true : false) : false ): void {} +type Function = (...args: any[]) => any; + type LoaderData = ReturnType>; type RouteLoaderData = ReturnType>; diff --git a/packages/remix-server-runtime/jsonify.ts b/packages/remix-server-runtime/jsonify.ts index baf75deb551..df60eef03ea 100644 --- a/packages/remix-server-runtime/jsonify.ts +++ b/packages/remix-server-runtime/jsonify.ts @@ -1,3 +1,10 @@ +import { + expectType, + type Equal, + type Expect, + type MutualExtends, +} from "./typecheck"; + // prettier-ignore // `Jsonify` emulates `let y = JSON.parse(JSON.stringify(x))`, but for types // so that we can infer the shape of the data sent over the network. @@ -213,25 +220,7 @@ expectType<{ a: string; recur?: Jsonify }>( recursive.recur!.recur!.recur! ); -// testing utils ---------------------------------------------------- - -// typecheck that expression is assignable to type -function expectType(_expression: T) {} - -// prettier-ignore -// adapted from https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts -type Equal = - (() => T extends X ? 1 : 2) extends - (() => T extends Y ? 1 : 2) ? true : false - -// adapted from https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts -type Expect = T; - -// looser, lazy equality check for recursive types -// prettier-ignore -type MutualExtends = [A] extends [B] ? [B] extends [A] ? true : false : false - -// general utils ---------------------------------------------------- +// utils ------------------------------------------------------------ type Pretty = { [K in keyof T]: T[K] }; diff --git a/packages/remix-server-runtime/serialize.ts b/packages/remix-server-runtime/serialize.ts index fb35d54cace..00743fbaac5 100644 --- a/packages/remix-server-runtime/serialize.ts +++ b/packages/remix-server-runtime/serialize.ts @@ -1,10 +1,7 @@ -import type { Jsonify } from "type-fest"; - -import type { AppData } from "./data"; +import type { Jsonify } from "./jsonify"; import type { TypedDeferredData, TypedResponse } from "./responses"; - -// Note: The return value has to be `any` and not `unknown` so it can match `void`. -type Fn = (...args: any[]) => any; +import { expectType } from "./typecheck"; +import { type Expect, type Equal } from "./typecheck"; // prettier-ignore /** @@ -13,29 +10,77 @@ type Fn = (...args: any[]) => any; * For example: * `type LoaderData = SerializeFrom` */ -export type SerializeFrom = - T extends (...args: any[]) => infer Output ? - Awaited extends TypedResponse ? Jsonify : - Awaited extends TypedDeferredData ? - // top-level promises - & { - [K in keyof U as - K extends symbol ? never : - Promise extends U[K] ? K : - never - ]: - // use generic to distribute over union - DeferValue - } - // non-promises - & Jsonify<{ [K in keyof U as Promise extends U[K] ? never : K]: U[K] }> - : - Jsonify> : +export type SerializeFrom = + T extends (...args: any[]) => infer Output ? Serialize> : + // Back compat: manually defined data type, not inferred from loader nor action Jsonify> ; +// note: cannot be inlined as logic requires union distribution +type Serialize = Output extends TypedDeferredData + ? // top-level promises + { + [K in keyof U as K extends symbol + ? never + : Promise extends U[K] + ? K + : never]: DeferValue; // use generic to distribute over union + } & Jsonify<{ + // non-promises + [K in keyof U as Promise extends U[K] ? never : K]: U[K]; + }> + : Output extends TypedResponse + ? Jsonify + : Jsonify; + // prettier-ignore type DeferValue = T extends undefined ? undefined : T extends Promise ? Promise>> : Jsonify; + +// tests ------------------------------------------------------------ + +type Pretty = { [K in keyof T]: T[K] }; + +type Loader = () => Promise< + | TypedResponse // returned responses + | TypedResponse // thrown responses +>; + +type LoaderDefer> = () => Promise< + | TypedDeferredData // returned responses + | TypedResponse // thrown responses +>; + +// prettier-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type _tests = [ + // back compat: plain object + Expect>, {a: string}>>, + + // plain response + Expect Response>, any>>, + + // only thrown responses (e.g. redirects) + Expect>, never>>, + + // basic loader data + Expect>>, {a: string}>>, + + // infer data type from `toJSON` + Expect>>, {a: string}>>, + + // regression test for specific field names + Expect>>, {a: string, name: number, data: boolean}>>, + + // defer top-level promises + Expect}>> extends {a: string, lazy: Promise<{ b: number }>} ? true : false> +]; + +// recursive +type Recursive = { a: string; recur?: Recursive }; +declare const recursive: SerializeFrom>; +expectType<{ a: string; recur?: Jsonify }>( + recursive.recur!.recur!.recur! +); diff --git a/packages/remix-server-runtime/typecheck.ts b/packages/remix-server-runtime/typecheck.ts new file mode 100644 index 00000000000..eff68677690 --- /dev/null +++ b/packages/remix-server-runtime/typecheck.ts @@ -0,0 +1,15 @@ +// typecheck that expression is assignable to type + +export function expectType(_expression: T) {} +// prettier-ignore +// adapted from https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts +export type Equal = + (() => T extends X ? 1 : 2) extends + (() => T extends Y ? 1 : 2) ? true : false + +// adapted from https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts +export type Expect = T; + +// looser, lazy equality check for recursive types +// prettier-ignore +export type MutualExtends = [A] extends [B] ? [B] extends [A] ? true : false : false From 4c64d18435993d9aa0b1dfcb9aac1653bab28d2b Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Tue, 10 Oct 2023 12:37:48 -0400 Subject: [PATCH 4/9] add test for jsonifying classes also remove redundant test for hook types --- .../remix-react/__tests__/hook-types-test.tsx | 327 ------------------ packages/remix-server-runtime/jsonify.ts | 13 + 2 files changed, 13 insertions(+), 327 deletions(-) delete mode 100644 packages/remix-react/__tests__/hook-types-test.tsx diff --git a/packages/remix-react/__tests__/hook-types-test.tsx b/packages/remix-react/__tests__/hook-types-test.tsx deleted file mode 100644 index 229f3c65f5d..00000000000 --- a/packages/remix-react/__tests__/hook-types-test.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import type { - TypedDeferredData, - TypedResponse, -} from "@remix-run/server-runtime"; - -import type { useLoaderData, useRouteLoaderData } from "../components"; - -function isEqual( - _arg: A extends B ? (B extends A ? true : false) : false -): void {} - -type Function = (...args: any[]) => any; - -type LoaderData = ReturnType>; -type RouteLoaderData = ReturnType>; - -describe("useLoaderData", () => { - it("supports plain data type", () => { - type AppData = { hello: string }; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); - - it("supports plain Response", () => { - type Loader = (args: any) => Response; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); - - it("infers type regardless of redirect", () => { - type Loader = ( - args: any - ) => TypedResponse<{ id: string }> | TypedResponse; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); - - it("supports Response-returning loader", () => { - type Loader = (args: any) => TypedResponse<{ hello: string }>; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); - - it("supports async Response-returning loader", () => { - type Loader = (args: any) => Promise>; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); - - it("supports data-returning loader", () => { - type Loader = (args: any) => { hello: string }; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); - - it("supports async data-returning loader", () => { - type Loader = (args: any) => Promise<{ hello: string }>; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); -}); - -describe("type serializer", () => { - it("converts Date to string", () => { - type AppData = { hello: Date }; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); - - it("supports custom toJSON", () => { - type AppData = { toJSON(): { data: string[] } }; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); - - it("supports recursion", () => { - type AppData = { dob: Date; parent: AppData }; - type SerializedAppData = { dob: string; parent: SerializedAppData }; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); - - it("supports tuples and arrays", () => { - type AppData = { arr: Date[]; tuple: [string, number, Date]; empty: [] }; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual< - response, - { arr: string[]; tuple: [string, number, string]; empty: [] } - >(true); - isEqual(true); - }); - - it("transforms unserializables to null in arrays", () => { - type AppData = [Function, symbol, undefined]; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); - - it("transforms unserializables to never in objects", () => { - type AppData = { arg1: Function; arg2: symbol; arg3: undefined }; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); - - it("supports class instances", () => { - class Test { - arg: string; - speak: () => string; - } - type Loader = (args: any) => TypedResponse; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); - - it("makes keys optional if the value is undefined", () => { - type AppData = { - arg1: string; - arg2: number | undefined; - arg3: undefined; - }; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual(true); - isEqual(true); - }); - - it("allows data key in value", () => { - type AppData = { data: { hello: string } }; - type response = LoaderData; - isEqual(true); - }); -}); - -describe("deferred type serializer", () => { - it("supports synchronous loader", () => { - type Loader = ( - args: any - ) => TypedDeferredData<{ hello: string; lazy: Promise }>; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual }>(true); - isEqual(true); - }); - - it("supports asynchronous loader", () => { - type Loader = ( - args: any - ) => Promise }>>; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual }>(true); - isEqual(true); - }); - - it("supports synchronous loader with deferred object result", () => { - type Loader = ( - args: any - ) => TypedDeferredData<{ hello: string; lazy: Promise<{ a: number }> }>; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual }>(true); - isEqual(true); - }); - - it("supports asynchronous loader with deferred object result", () => { - type Loader = ( - args: any - ) => Promise< - TypedDeferredData<{ hello: string; lazy: Promise<{ a: number }> }> - >; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual }>(true); - isEqual(true); - }); - - it("converts Date to string", () => { - type Loader = ( - args: any - ) => Promise< - TypedDeferredData<{ hello: Date; lazy: Promise<{ a: Date }> }> - >; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual }>(true); - isEqual(true); - }); - - it("supports custom toJSON", () => { - type AppData = { toJSON(): { data: string[] } }; - type Loader = ( - args: any - ) => Promise< - TypedDeferredData<{ hello: AppData; lazy: Promise<{ a: AppData }> }> - >; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual< - response, - { hello: { data: string[] }; lazy: Promise<{ a: { data: string[] } }> } - >(true); - isEqual(true); - }); - - it("supports recursion", () => { - type AppData = { dob: Date; parent: AppData }; - type SerializedAppData = { dob: string; parent: SerializedAppData }; - type Loader = ( - args: any - ) => Promise }>>; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual< - response, - { hello: SerializedAppData; lazy: Promise } - >(true); - isEqual(true); - }); - - it("supports tuples and arrays", () => { - type AppData = { arr: Date[]; tuple: [string, number, Date]; empty: [] }; - type SerializedAppData = { - arr: string[]; - tuple: [string, number, string]; - empty: []; - }; - type Loader = ( - args: any - ) => Promise }>>; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual< - response, - { hello: SerializedAppData; lazy: Promise } - >(true); - isEqual(true); - }); - - it("transforms unserializables to null in arrays", () => { - type AppData = [Function, symbol, undefined]; - type SerializedAppData = [null, null, null]; - type Loader = ( - args: any - ) => Promise }>>; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual< - response, - { hello: SerializedAppData; lazy: Promise } - >(true); - isEqual(true); - }); - - it("transforms unserializables to never in objects", () => { - type AppData = { arg1: Function; arg2: symbol; arg3: undefined }; - type Loader = ( - args: any - ) => Promise }>>; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual }>(true); - isEqual(true); - }); - - it("supports class instances", () => { - class Test { - arg: string; - speak: () => string; - } - type Loader = ( - args: any - ) => Promise }>>; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual< - response, - { hello: { arg: string }; lazy: Promise<{ arg: string }> } - >(true); - isEqual(true); - }); - - it("makes keys optional if the value is undefined", () => { - type AppData = { - arg1: string; - arg2: number | undefined; - arg3: undefined; - }; - type SerializedAppData = { arg1: string; arg2?: number }; - type Loader = ( - args: any - ) => Promise }>>; - type response = LoaderData; - type routeResponse = RouteLoaderData; - isEqual< - response, - { hello: SerializedAppData; lazy: Promise } - >(true); - isEqual(true); - }); -}); diff --git a/packages/remix-server-runtime/jsonify.ts b/packages/remix-server-runtime/jsonify.ts index df60eef03ea..0a028a499a3 100644 --- a/packages/remix-server-runtime/jsonify.ts +++ b/packages/remix-server-runtime/jsonify.ts @@ -209,8 +209,21 @@ type _tests = [ Expect>, {a: never}>>, Expect>, {a: never, b:string}>>, Expect>, {a: never, b: string} | {a: string, b: never}>>, + + // class + Expect>, {a: string}>>, ]; +class MyClass { + a: string; + b: () => string; + + constructor() { + this.a = "hello"; + this.b = () => "world"; + } +} + type Recursive = { a: Date; recur?: Recursive; From ed9533385e14c3628771bafdbf06c425dcde559f Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Tue, 10 Oct 2023 13:05:01 -0400 Subject: [PATCH 5/9] remove no-op test testing `any` type doesn't actually test anything --- packages/remix-server-runtime/serialize.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/remix-server-runtime/serialize.ts b/packages/remix-server-runtime/serialize.ts index 00743fbaac5..d95a7bf6c79 100644 --- a/packages/remix-server-runtime/serialize.ts +++ b/packages/remix-server-runtime/serialize.ts @@ -59,9 +59,6 @@ type _tests = [ // back compat: plain object Expect>, {a: string}>>, - // plain response - Expect Response>, any>>, - // only thrown responses (e.g. redirects) Expect>, never>>, From 07a1a62544efc863cbd9c9ae7ceda88758415e2d Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Tue, 10 Oct 2023 13:27:29 -0400 Subject: [PATCH 6/9] Prioritize `toJSON` Co-authored-by: Nicholas Rakoto --- packages/remix-server-runtime/jsonify.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/remix-server-runtime/jsonify.ts b/packages/remix-server-runtime/jsonify.ts index 0a028a499a3..80a6c2b5afd 100644 --- a/packages/remix-server-runtime/jsonify.ts +++ b/packages/remix-server-runtime/jsonify.ts @@ -12,6 +12,9 @@ export type Jsonify = // any IsAny extends true ? any : + // toJSON + T extends { toJSON(): infer U } ? (U extends JsonValue ? U : unknown) : + // primitives T extends JsonPrimitive ? T : T extends String ? string : @@ -28,9 +31,6 @@ export type Jsonify = // Not JSON serializable T extends NotJson ? never : - // toJSON - T extends { toJSON(): infer U } ? (U extends JsonValue ? U : unknown) : - // tuple & array T extends [] ? [] : T extends readonly [infer F, ...infer R] ? [NeverToNull>, ...Jsonify] : @@ -148,6 +148,8 @@ type _tests = [ Expect, string>>, Expect, unknown>>, Expect, unknown>>, + Expect, string>>, + // tuple & array Expect, []>>, @@ -224,6 +226,7 @@ class MyClass { } } +// real-world example: `InvoiceLineItem` from `stripe` type Recursive = { a: Date; recur?: Recursive; @@ -233,6 +236,11 @@ expectType<{ a: string; recur?: Jsonify }>( recursive.recur!.recur!.recur! ); +// real-world example: `Temporal` from `@js-temporal/polyfill` +interface BooleanWithToJson extends Boolean { + toJSON(): string; +} + // utils ------------------------------------------------------------ type Pretty = { [K in keyof T]: T[K] }; From e2ea774f01c3fe33178f26b31371dadf646cfba9 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Tue, 10 Oct 2023 13:35:36 -0400 Subject: [PATCH 7/9] add changeset --- .changeset/stale-apples-reply.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/stale-apples-reply.md diff --git a/.changeset/stale-apples-reply.md b/.changeset/stale-apples-reply.md new file mode 100644 index 00000000000..17bff0bcf6e --- /dev/null +++ b/.changeset/stale-apples-reply.md @@ -0,0 +1,11 @@ +--- +"@remix-run/react": patch +"@remix-run/server-runtime": patch +--- + +Emulate types for `JSON.parse(JSON.stringify(x))` in `SerializeFrom` + +Notably, type fields that are only assignable to `undefined` after serialization are now omitted since +`JSON.stringify |> JSON.parse` will omit them. See test cases for examples. + +Also fixes type errors when upgrading to v2 from 1.19 From 99e93acfd5292cd14e124c33a11b1027df1b7095 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Tue, 10 Oct 2023 22:10:54 -0400 Subject: [PATCH 8/9] formatting --- packages/remix-server-runtime/serialize.ts | 34 +++++++++++++--------- packages/remix-server-runtime/typecheck.ts | 2 +- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/remix-server-runtime/serialize.ts b/packages/remix-server-runtime/serialize.ts index d95a7bf6c79..4a2a8a3a79c 100644 --- a/packages/remix-server-runtime/serialize.ts +++ b/packages/remix-server-runtime/serialize.ts @@ -17,21 +17,27 @@ export type SerializeFrom = ; // note: cannot be inlined as logic requires union distribution -type Serialize = Output extends TypedDeferredData - ? // top-level promises - { - [K in keyof U as K extends symbol - ? never - : Promise extends U[K] - ? K - : never]: DeferValue; // use generic to distribute over union - } & Jsonify<{ - // non-promises - [K in keyof U as Promise extends U[K] ? never : K]: U[K]; +// prettier-ignore +type Serialize = + Output extends TypedDeferredData ? + // top-level promises + & { + [K in keyof U as + K extends symbol ? never : + Promise extends U[K] ? K : + never + ]: DeferValue; // use generic to distribute over union + } + // non-promises + & Jsonify<{ + [K in keyof U as + Promise extends U[K] ? never : + K + ]: U[K]; }> - : Output extends TypedResponse - ? Jsonify - : Jsonify; + : + Output extends TypedResponse ? Jsonify : + Jsonify; // prettier-ignore type DeferValue = diff --git a/packages/remix-server-runtime/typecheck.ts b/packages/remix-server-runtime/typecheck.ts index eff68677690..7f6ffeab236 100644 --- a/packages/remix-server-runtime/typecheck.ts +++ b/packages/remix-server-runtime/typecheck.ts @@ -1,6 +1,6 @@ // typecheck that expression is assignable to type - export function expectType(_expression: T) {} + // prettier-ignore // adapted from https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts export type Equal = From be4cb0743826aef7240532f5c5f90f8b6522e0fd Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Tue, 10 Oct 2023 22:12:22 -0400 Subject: [PATCH 9/9] remove unused type-fest dep --- packages/remix-server-runtime/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 9dff84ea853..62819f396d7 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -21,8 +21,7 @@ "@web3-storage/multipart-parser": "^1.0.0", "cookie": "^0.4.1", "set-cookie-parser": "^2.4.8", - "source-map": "^0.7.3", - "type-fest": "^4.0.0" + "source-map": "^0.7.3" }, "devDependencies": { "@types/set-cookie-parser": "^2.4.1",