From 5a38ebaae371b61cfb1d2ed437c96895cd2fc288 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Wed, 28 Aug 2024 15:54:16 -0400 Subject: [PATCH] Single-fetch typesafety (#9893) --- .changeset/moody-cups-give.md | 40 +++ docs/guides/single-fetch.md | 181 ++++++------- integration/single-fetch-test.ts | 50 +++- packages/remix-cloudflare/index.ts | 2 - packages/remix-deno/index.ts | 2 - packages/remix-node/index.ts | 2 - packages/remix-react/future/single-fetch.d.ts | 73 ----- packages/remix-react/index.tsx | 8 - packages/remix-react/package.json | 2 +- packages/remix-react/rollup.config.js | 1 - packages/remix-react/single-fetch.tsx | 30 +-- packages/remix-server-runtime/future.ts | 1 + packages/remix-server-runtime/index.ts | 7 +- packages/remix-server-runtime/package.json | 2 +- packages/remix-server-runtime/serialize.ts | 12 +- packages/remix-server-runtime/single-fetch.ts | 254 ++++++++++++++++-- pnpm-lock.yaml | 12 +- 17 files changed, 423 insertions(+), 256 deletions(-) create mode 100644 .changeset/moody-cups-give.md delete mode 100644 packages/remix-react/future/single-fetch.d.ts create mode 100644 packages/remix-server-runtime/future.ts diff --git a/.changeset/moody-cups-give.md b/.changeset/moody-cups-give.md new file mode 100644 index 00000000000..9755d05714e --- /dev/null +++ b/.changeset/moody-cups-give.md @@ -0,0 +1,40 @@ +--- +"@remix-run/cloudflare": patch +"@remix-run/deno": patch +"@remix-run/node": patch +"@remix-run/react": patch +"@remix-run/server-runtime": patch +--- + +(unstable) Improved typesafety for single-fetch + +If you were already using single-fetch types: + +- Remove `"@remix-run/react/future/single-fetch.d.ts"` override from `tsconfig.json` > `compilerOptions` > `types` +- Remove `defineLoader`, `defineAction`, `defineClientLoader`, `defineClientAction` helpers from your route modules +- Replace `UIMatch_SingleFetch` type helper with `UIMatch` +- Replace `MetaArgs_SingleFetch` type helper with `MetaArgs` + +Then you are ready for the new typesafety setup: + +```ts +// vite.config.ts + +declare module "@remix-run/server-runtime" { + interface Future { + unstable_singleFetch: true // 👈 enable _types_ for single-fetch + } +} + +export default defineConfig({ + plugins: [ + remix({ + future: { + unstable_singleFetch: true // 👈 enable single-fetch + } + }) + ] +}) +``` + +For more information, see [Guides > Single Fetch](https://remix.run/docs/en/dev/guides/single-fetch) in our docs. diff --git a/docs/guides/single-fetch.md b/docs/guides/single-fetch.md index 22fc56c0496..9a26002f830 100644 --- a/docs/guides/single-fetch.md +++ b/docs/guides/single-fetch.md @@ -145,145 +145,120 @@ Without Single Fetch, any plain Javascript object returned from a `loader` or `a With Single Fetch, naked objects will be streamed directly, so the built-in type inference is no longer accurate once you have opted-into Single Fetch. For example, they would assume that a `Date` would be serialized to a string on the client 😕. -In order to ensure you get the proper types when using Single Fetch, we've included a set of type overrides that you can include in your `tsconfig.json`'s `compilerOptions.types` array which aligns the types with the Single Fetch behavior: - -```json -{ - "compilerOptions": { - //... - "types": [ - // ... - "@remix-run/react/future/single-fetch.d.ts" - ] +#### Enable Single Fetch types + +To switch over to Single Fetch types, you should augment Remix's `Future` interface with `unstable_singleFetch: true`: + +```ts filename=vite.config.ts +declare module "@remix-run/server-runtime" { + interface Future { + unstable_singleFetch: true; } } ``` -🚨 Make sure the single-fetch types come after any other Remix packages in `types` so that they override those existing types. - -#### Loader/Action Definition Utilities - -To enhance type-safety when defining loaders and actions with Single Fetch, you can use the new `unstable_defineLoader` and `unstable_defineAction` utilities: +Now `useLoaderData`, `useActionData`, and any other utilities that use a `typeof loader` generic should be using Single Fetch types: ```ts -import { unstable_defineLoader as defineLoader } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; -export const loader = defineLoader(({ request }) => { - // ^? Request -}); +export function loader() { + return { + planet: "world", + date: new Date(), + }; +} + +export default function Component() { + const data = useLoaderData(); + // ^? { planet: string, date: Date } +} ``` -Not only does this give you types for arguments (and deprecates `LoaderFunctionArgs`), but it also ensures you are returning single-fetch compatible types: +#### Functions and class instances -```ts -export const loader = defineLoader(() => { - return { hello: "world", badData: () => 1 }; - // ^^^^^^^ Type error: `badData` is not serializable -}); +In general, functions cannot be reliably sent over the network, so they get serialized as `undefined`: -export const action = defineAction(() => { - return { hello: "world", badData: new CustomType() }; - // ^^^^^^^ Type error: `badData` is not serializable -}); -``` +```ts +import { useLoaderData } from "@remix-run/react"; -Single-fetch supports the following return types: +export function loader() { + return { + planet: "world", + date: new Date(), + notSoRandom: () => 7, + }; +} -```ts -type Serializable = - | undefined - | null - | boolean - | string - | symbol - | number - | bigint - | Date - | URL - | RegExp - | Error - | Array - | { [key: PropertyKey]: Serializable } // objects with serializable values - | Map - | Set - | Promise; +export default function Component() { + const data = useLoaderData(); + // ^? { planet: string, date: Date, notSoRandom: undefined } +} ``` -There are also client-side equivalents un `defineClientLoader`/`defineClientAction` that don't have the same return value restrictions because data returned from `clientLoader`/`clientAction` does not need to be serialized over the wire: +Methods are also not serializable, so class instances get slimmed down to just their serializable properties: ```ts -import { unstable_defineLoader as defineLoader } from "@remix-run/node"; -import { unstable_defineClientLoader as defineClientLoader } from "@remix-run/react"; +import { useLoaderData } from "@remix-run/react"; -export const loader = defineLoader(() => { - return { msg: "Hello!", date: new Date() }; -}); +class Dog { + name: string; + age: number; -export const clientLoader = defineClientLoader( - async ({ serverLoader }) => { - const data = await serverLoader(); - // ^? { msg: string, date: Date } - return { - ...data, - client: "World!", - }; + constructor(name: string, age: number) { + this.name = name; + this.age = age; } -); -export default function Component() { - const data = useLoaderData(); - // ^? { msg: string, date: Date, client: string } + bark() { + console.log("woof"); + } } -``` -These utilities are primarily for type inference on `useLoaderData` and its equivalents. If you have a resource route that returns a `Response` and is not consumed by Remix APIs (such as `useFetcher`), then you can just stick with your normal `loader`/`action` definitions. Converting those routes to use `defineLoader`/`defineAction` would cause type errors because `turbo-stream` cannot serialize a `Response` instance. - -#### `useLoaderData`, `useActionData`, `useRouteLoaderData`, `useFetcher` - -These methods do not require any code changes on your part - adding the Single Fetch types will cause their generics to deserialize correctly: - -```ts -export const loader = defineLoader(async () => { - const data = await fetchSomeData(); +export function loader() { return { - message: data.message, // <- string - date: data.date, // <- Date + planet: "world", + date: new Date(), + spot: new Dog("Spot", 3), }; -}); +} export default function Component() { - // ❌ Before Single Fetch, types were serialized via JSON.stringify const data = useLoaderData(); - // ^? { message: string, date: string } - - // ✅ With Single Fetch, types are serialized via turbo-stream - const data = useLoaderData(); - // ^? { message: string, date: Date } + // ^? { planet: string, date: Date, spot: { name: string, age: number, bark: undefined } } } ``` -#### `useMatches` +#### `clientLoader` and `clientAction` -`useMatches` requires a manual cast to specify the loader type in order to get proper type inference on a given `match.data`. When using Single Fetch, you will need to replace the `UIMatch` type with `UIMatch_SingleFetch`: +Make sure to include types for the `clientLoader` args and `clientAction` args as that is how our types detect client data functions. -```diff - let matches = useMatches(); -- let rootMatch = matches[0] as UIMatch; -+ let rootMatch = matches[0] as UIMatch_SingleFetch; -``` +Data from client-side loaders and actions are never serialized so types for those are preserved: -#### `meta` Function +```ts +import { + useLoaderData, + type ClientLoaderFunctionArgs, +} from "@remix-run/react"; -`meta` functions also require a generic to indicate the current and ancestor route loader types in order to properly type the `data` and `matches` parameters. When using Single Fetch, you will need to replace the `MetaArgs` type with `MetaArgs_SingleFetch`: +class Dog { + /* ... */ +} -```diff - export function meta({ - data, - matches, -- }: MetaArgs) { -+ }: MetaArgs_SingleFetch) { - // ... - } +// Make sure to annotate the types for the args! 👇 +export function clientLoader(_: ClientLoaderFunctionArgs) { + return { + planet: "world", + date: new Date(), + notSoRandom: () => 7, + spot: new Dog("Spot", 3), + }; +} + +export default function Component() { + const data = useLoaderData(); + // ^? { planet: string, date: Date, notSoRandom: () => number, spot: Dog } +} ``` ### Headers diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts index fbcc47f4152..503ec75689b 100644 --- a/integration/single-fetch-test.ts +++ b/integration/single-fetch-test.ts @@ -71,6 +71,18 @@ const files = { }; } + class MyClass { + a: string + b: bigint + + constructor(a: string, b: bigint) { + this.a = a + this.b = b + } + + c() {} + } + export function loader({ request }) { if (new URL(request.url).searchParams.has("error")) { throw new Error("Loader Error"); @@ -78,6 +90,10 @@ const files = { return { message: "DATA", date: new Date("${ISO_DATE}"), + unserializable: { + function: () => {}, + class: new MyClass("hello", BigInt(1)), + }, }; } @@ -113,6 +129,18 @@ const files = { }, { status: 201, headers: { 'X-Action': 'yes' }}); } + class MyClass { + a: string + b: Date + + constructor(a: string, b: Date) { + this.a = a + this.b = b + } + + c() {} + } + export function loader({ request }) { if (new URL(request.url).searchParams.has("error")) { throw new Error("Loader Error"); @@ -120,6 +148,10 @@ const files = { return data({ message: "DATA", date: new Date("${ISO_DATE}"), + unserializable: { + function: () => {}, + class: new MyClass("hello", BigInt(1)), + }, }, { status: 206, headers: { 'X-Loader': 'yes' }}); } @@ -175,7 +207,7 @@ test.describe("single-fetch", () => { expect(res.headers.get("Content-Type")).toBe("text/x-script"); res = await fixture.requestSingleFetchData("/data.data"); - expect(res.data).toEqual({ + expect(res.data).toStrictEqual({ root: { data: { message: "ROOT", @@ -185,6 +217,13 @@ test.describe("single-fetch", () => { data: { message: "DATA", date: new Date(ISO_DATE), + unserializable: { + function: undefined, + class: { + a: "hello", + b: BigInt(1), + }, + }, }, }, }); @@ -255,7 +294,7 @@ test.describe("single-fetch", () => { let res = await fixture.requestSingleFetchData("/data-with-response.data"); expect(res.status).toEqual(206); expect(res.headers.get("X-Loader")).toEqual("yes"); - expect(res.data).toEqual({ + expect(res.data).toStrictEqual({ root: { data: { message: "ROOT", @@ -265,6 +304,13 @@ test.describe("single-fetch", () => { data: { message: "DATA", date: new Date(ISO_DATE), + unserializable: { + function: undefined, + class: { + a: "hello", + b: BigInt(1), + }, + }, }, }, }); diff --git a/packages/remix-cloudflare/index.ts b/packages/remix-cloudflare/index.ts index 996f5f6749e..ec5fa2a0dc6 100644 --- a/packages/remix-cloudflare/index.ts +++ b/packages/remix-cloudflare/index.ts @@ -13,8 +13,6 @@ export { createRequestHandler, createSession, unstable_data, - unstable_defineLoader, - unstable_defineAction, defer, broadcastDevReady, logDevReady, diff --git a/packages/remix-deno/index.ts b/packages/remix-deno/index.ts index 7e7cbbb2c09..c82e1b0ae6d 100644 --- a/packages/remix-deno/index.ts +++ b/packages/remix-deno/index.ts @@ -28,8 +28,6 @@ export { unstable_composeUploadHandlers, unstable_createMemoryUploadHandler, unstable_data, - unstable_defineAction, - unstable_defineLoader, unstable_parseMultipartFormData, } from "@remix-run/server-runtime"; diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index 0665a313185..960d3bad3e7 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -25,8 +25,6 @@ export { createRequestHandler, createSession, unstable_data, - unstable_defineLoader, - unstable_defineAction, defer, broadcastDevReady, logDevReady, diff --git a/packages/remix-react/future/single-fetch.d.ts b/packages/remix-react/future/single-fetch.d.ts deleted file mode 100644 index 165b318569c..00000000000 --- a/packages/remix-react/future/single-fetch.d.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { - MetaArgs, - UIMatch, - UNSAFE_MetaMatch, - unstable_ClientLoader as ClientLoader, - unstable_ClientAction as ClientAction, -} from "@remix-run/react"; -import type { - unstable_Loader as Loader, - unstable_Action as Action, - unstable_Serialize as Serialize, -} from "@remix-run/server-runtime"; -import type { - useFetcher as useFetcherRR, - FetcherWithComponents, -} from "react-router-dom"; - -declare module "@remix-run/react" { - export function useLoaderData< - T extends Loader | ClientLoader - >(): T extends Loader ? Serialize : Awaited>; - - export function useActionData(): - | (T extends Action ? Serialize : Awaited>) - | undefined; - - export function useRouteLoaderData( - routeId: string - ): Serialize | undefined; - - export function useFetcher( - opts?: Parameters[0] - ): FetcherWithComponents>; - - export type UIMatch_SingleFetch = Omit< - UIMatch, - "data" - > & { - data: D extends Loader ? Serialize : never; - }; - - interface MetaMatch_SingleFetch< - RouteId extends string = string, - L extends Loader | unknown = unknown - > extends Omit, "data"> { - data: L extends Loader ? Serialize : unknown; - } - - type MetaMatches_SingleFetch< - MatchLoaders extends Record = Record< - string, - unknown - > - > = Array< - { - [K in keyof MatchLoaders]: MetaMatch_SingleFetch< - Exclude, - MatchLoaders[K] - >; - }[keyof MatchLoaders] - >; - - export interface MetaArgs_SingleFetch< - L extends Loader | unknown = unknown, - MatchLoaders extends Record = Record< - string, - unknown - > - > extends Omit, "data" | "matches"> { - data: (L extends Loader ? Serialize : unknown) | undefined; - matches: MetaMatches_SingleFetch; - } -} diff --git a/packages/remix-react/index.tsx b/packages/remix-react/index.tsx index 8d8809ca644..bd966bfcd79 100644 --- a/packages/remix-react/index.tsx +++ b/packages/remix-react/index.tsx @@ -113,14 +113,6 @@ export { ScrollRestoration } from "./scroll-restoration"; export type { RemixServerProps } from "./server"; export { RemixServer } from "./server"; -export type { - ClientAction as unstable_ClientAction, - ClientLoader as unstable_ClientLoader, -} from "./single-fetch"; -export { - defineClientAction as unstable_defineClientAction, - defineClientLoader as unstable_defineClientLoader, -} from "./single-fetch"; export type { FutureConfig as UNSAFE_FutureConfig, diff --git a/packages/remix-react/package.json b/packages/remix-react/package.json index 931399403e5..67817bb7a4f 100644 --- a/packages/remix-react/package.json +++ b/packages/remix-react/package.json @@ -23,7 +23,7 @@ "@remix-run/server-runtime": "workspace:*", "react-router": "0.0.0-experimental-7d87ffb8c", "react-router-dom": "0.0.0-experimental-7d87ffb8c", - "turbo-stream": "2.3.0" + "turbo-stream": "2.4.0" }, "devDependencies": { "@remix-run/node": "workspace:*", diff --git a/packages/remix-react/rollup.config.js b/packages/remix-react/rollup.config.js index 20d9673a71f..5ad9ecd1c06 100644 --- a/packages/remix-react/rollup.config.js +++ b/packages/remix-react/rollup.config.js @@ -51,7 +51,6 @@ module.exports = function rollup() { { src: "LICENSE.md", dest: [outputDir, sourceDir] }, { src: `${sourceDir}/package.json`, dest: outputDir }, { src: `${sourceDir}/README.md`, dest: outputDir }, - { src: `${sourceDir}/future`, dest: outputDir }, ], }), copyToPlaygrounds(), diff --git a/packages/remix-react/single-fetch.tsx b/packages/remix-react/single-fetch.tsx index a49bd1e4648..927f329cf82 100644 --- a/packages/remix-react/single-fetch.tsx +++ b/packages/remix-react/single-fetch.tsx @@ -1,7 +1,5 @@ import * as React from "react"; import type { - ActionFunctionArgs as RRActionArgs, - LoaderFunctionArgs as RRLoaderArgs, unstable_DataStrategyFunction as DataStrategyFunction, unstable_HandlerResult as HandlerResult, } from "@remix-run/router"; @@ -14,9 +12,6 @@ import { import type { UNSAFE_SingleFetchResult as SingleFetchResult, UNSAFE_SingleFetchResults as SingleFetchResults, - unstable_Action, - unstable_Loader, - unstable_Serialize, } from "@remix-run/server-runtime"; import { UNSAFE_SingleFetchRedirectSymbol as SingleFetchRedirectSymbol } from "@remix-run/server-runtime"; import type { @@ -31,23 +26,6 @@ import { escapeHtml } from "./markup"; import type { RouteModules } from "./routeModules"; import invariant from "./invariant"; -// clientLoader -type ClientLoaderArgs = RRLoaderArgs & { - serverLoader: () => Promise>; -}; -export type ClientLoader = (args: ClientLoaderArgs) => unknown; -export let defineClientLoader = ( - clientLoader: T -): T & { hydrate?: boolean } => clientLoader; - -// clientAction -type ClientActionArgs = RRActionArgs & { - serverAction: () => Promise>; -}; -export type ClientAction = (args: ClientActionArgs) => unknown; -export let defineClientAction = (clientAction: T): T => - clientAction; - interface StreamTransferProps { context: EntryContext; identifier: number; @@ -380,6 +358,14 @@ export function decodeViaTurboStream( return { value: { [SingleFetchRedirectSymbol]: rest[0] } }; } }, + (type, value) => { + if (type === "SingleFetchFallback") { + return { value: undefined }; + } + if (type === "SingleFetchClassInstance") { + return { value }; + } + }, ], }); } diff --git a/packages/remix-server-runtime/future.ts b/packages/remix-server-runtime/future.ts new file mode 100644 index 00000000000..3dd46a2bce1 --- /dev/null +++ b/packages/remix-server-runtime/future.ts @@ -0,0 +1 @@ +export interface Future {} diff --git a/packages/remix-server-runtime/index.ts b/packages/remix-server-runtime/index.ts index b4ec1d6678b..cb084d982f7 100644 --- a/packages/remix-server-runtime/index.ts +++ b/packages/remix-server-runtime/index.ts @@ -9,13 +9,8 @@ export { defer, json, redirect, redirectDocument, replace } from "./responses"; export { SingleFetchRedirectSymbol as UNSAFE_SingleFetchRedirectSymbol, data as unstable_data, - defineLoader as unstable_defineLoader, - defineAction as unstable_defineAction, } from "./single-fetch"; export type { - Loader as unstable_Loader, - Action as unstable_Action, - Serialize as unstable_Serialize, SingleFetchResult as UNSAFE_SingleFetchResult, SingleFetchResults as UNSAFE_SingleFetchResults, } from "./single-fetch"; @@ -50,6 +45,8 @@ export type { RedirectFunction, } from "./interface"; +export type { Future } from "./future"; + // Remix server runtime packages should re-export these types export type { ActionFunction, diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index b4ba024313f..6f86e439891 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -25,7 +25,7 @@ "cookie": "^0.6.0", "set-cookie-parser": "^2.4.8", "source-map": "^0.7.3", - "turbo-stream": "2.3.0" + "turbo-stream": "2.4.0" }, "devDependencies": { "@types/set-cookie-parser": "^2.4.1", diff --git a/packages/remix-server-runtime/serialize.ts b/packages/remix-server-runtime/serialize.ts index c3d4822f498..7791d4c0bb3 100644 --- a/packages/remix-server-runtime/serialize.ts +++ b/packages/remix-server-runtime/serialize.ts @@ -6,6 +6,12 @@ import type { } from "./routeModules"; import { expectType } from "./typecheck"; import { type Expect, type Equal } from "./typecheck"; +import { type SerializeFrom as SingleFetch_SerializeFrom } from "./single-fetch"; +import type { Future } from "./future"; + +// prettier-ignore +type SingleFetchEnabled = + Future extends { unstable_singleFetch: infer T extends boolean } ? T : false // prettier-ignore /** @@ -17,6 +23,11 @@ import { type Expect, type Equal } from "./typecheck"; * `type LoaderData = SerializeFrom` */ export type SerializeFrom = + SingleFetchEnabled extends true ? + T extends (...args: []) => unknown ? + SingleFetch_SerializeFrom : + never + : T extends (...args: any[]) => infer Output ? Parameters extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] ? // Client data functions may not serialize @@ -27,7 +38,6 @@ export type SerializeFrom = : // Back compat: manually defined data type, not inferred from loader nor action Jsonify> -; // note: cannot be inlined as logic requires union distribution // prettier-ignore diff --git a/packages/remix-server-runtime/single-fetch.ts b/packages/remix-server-runtime/single-fetch.ts index b1c888f033a..ab2b5e91ecc 100644 --- a/packages/remix-server-runtime/single-fetch.ts +++ b/packages/remix-server-runtime/single-fetch.ts @@ -12,15 +12,20 @@ import { } from "@remix-run/router"; import { encode } from "turbo-stream"; +import { type Expect, type Equal } from "./typecheck"; import type { ServerBuild } from "./build"; import type { AppLoadContext } from "./data"; import { sanitizeError, sanitizeErrors } from "./errors"; import { getDocumentHeaders } from "./headers"; import { ServerMode } from "./mode"; -import type { TypedDeferredData, TypedResponse } from "./responses"; +import type { TypedResponse } from "./responses"; import { isRedirectStatusCode, isResponse } from "./responses"; -import type { ActionFunctionArgs, LoaderFunctionArgs } from "./routeModules"; -import type { SerializeFrom } from "./serialize"; +import type { Jsonify } from "./jsonify"; +import type { + ClientActionFunctionArgs, + ClientLoaderFunctionArgs, + LoaderFunctionArgs, +} from "./routeModules"; export const SingleFetchRedirectSymbol = Symbol("SingleFetchRedirect"); @@ -351,6 +356,18 @@ export function encodeViaTurboStream( } }, ], + postPlugins: [ + (value) => { + if (!value) return; + if (typeof value !== "object") return; + + return [ + "SingleFetchClassInstance", + Object.fromEntries(Object.entries(value)), + ]; + }, + () => ["SingleFetchFallback"], + ], }); } @@ -361,8 +378,6 @@ export function data( return routerData(value, init); } -type MaybePromise = T | Promise; - type Serializable = | undefined | null @@ -370,40 +385,225 @@ type Serializable = | string | symbol | number - | Array - | { [key: PropertyKey]: Serializable } | bigint | Date | URL | RegExp | Error + | ReadonlyArray + | Array + | { [key: PropertyKey]: Serializable } | Map | Set | Promise; -type DataFunctionReturnValue = - | Serializable - | DataWithResponseInit - | TypedDeferredData> - | TypedResponse>; +// prettier-ignore +type Serialize = + T extends void ? undefined : + + // First, let type stay as-is if its already serializable... + T extends Serializable ? T : + + // ...then don't allow functions to be serialized... + T extends (...args: any[]) => unknown ? undefined : + + // ...lastly handle inner types for all container types allowed by `turbo-stream` + + // Promise + T extends Promise ? Promise> : + + // Map & Set + T extends Map ? Map, Serialize> : + T extends Set ? Set> : + + // Array + T extends [] ? [] : + T extends readonly [infer F, ...infer R] ? [Serialize, ...Serialize] : + T extends Array ? Array> : + T extends readonly unknown[] ? readonly Serialize[] : + + // Record + T extends Record ? {[K in keyof T]: Serialize} : + + undefined + +type Fn = (...args: any[]) => unknown; // Backwards-compatible type for Remix v2 where json/defer still use the old types, // and only non-json/defer returns use the new types. This allows for incremental // migration of loaders to return naked objects. In the next major version, // json/defer will be removed so everything will use the new simplified typings. // prettier-ignore -export type Serialize = - Awaited> extends TypedDeferredData ? D : - Awaited> extends TypedResponse> ? SerializeFrom : - Awaited> extends DataWithResponseInit ? D : - Awaited>; - -export type Loader = ( - args: LoaderFunctionArgs -) => MaybePromise; -export let defineLoader = (loader: T): T => loader; - -export type Action = ( - args: ActionFunctionArgs -) => MaybePromise; -export let defineAction = (action: T): T => action; +export type SerializeFrom = + Parameters extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] ? + ReturnType extends TypedResponse ? Jsonify : + Awaited> + : + Awaited> extends TypedResponse> ? Jsonify : + Awaited> extends DataWithResponseInit ? Serialize : + Serialize>>; + +type ServerLoader = (args: LoaderFunctionArgs) => T; +type ClientLoader = (args: ClientLoaderFunctionArgs) => T; + +class TestClass { + constructor(public a: string, public b: Date) { + this.a = a; + this.b = b; + } + + testmethod() {} +} + +interface TestInterface { + undefined: undefined; + null: null; + boolean: boolean; + string: string; + symbol: symbol; + number: number; + bigint: bigint; + Date: Date; + URL: URL; + RegExp: RegExp; + Error: Error; + Array: Array; + ReadonlyArray: ReadonlyArray; + Set: Set; + Map: Map; +} + +type Recursive = { + a: string; + b: Date; + recursive?: Recursive; +}; + +// prettier-ignore +// eslint-disable-next-line +type _tests = [ + Expect>, undefined>>, + Expect; + ReadonlyArray: ReadonlyArray; + Set: Set, + Map: Map, + TestInterface: TestInterface, + Recursive: Recursive + }>>, { + undefined: undefined, + null: null, + boolean: boolean, + string: string, + symbol: symbol, + number: number, + bigint: bigint, + Date: Date, + URL: URL, + RegExp: RegExp, + Error: Error, + Array: Array; + ReadonlyArray: ReadonlyArray; + Set: Set, + Map: Map, + TestInterface: TestInterface + Recursive: Recursive + }>>, + Expect, + ReadonlyArray, + Set, + Map, + ]>>, [ + undefined, + null, + boolean, + string, + symbol, + number, + bigint, + Date, + URL, + RegExp, + Error, + Array, + ReadonlyArray, + Set, + Map, + ]>>, + Expect, + ReadonlyArray, + Set, + Map, + ]>>>, [ + undefined, + null, + boolean, + string, + symbol, + number, + bigint, + Date, + URL, + RegExp, + Error, + Array, + ReadonlyArray, + Set, + Map, + ]>>, + + Expect void, + class: TestClass + }>>, { + function: undefined, + class: { + a: string + b: Date, + testmethod: undefined + }, + }>>, + + Expect void, + class: TestClass + }>>, { + function: () => void, + class: TestClass + }>>, +] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a201c04ea1c..ccae98be81a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1229,8 +1229,8 @@ importers: specifier: 0.0.0-experimental-7d87ffb8c version: 0.0.0-experimental-7d87ffb8c(react-dom@18.2.0)(react@18.2.0) turbo-stream: - specifier: 2.3.0 - version: 2.3.0 + specifier: 2.4.0 + version: 2.4.0 devDependencies: '@remix-run/node': specifier: workspace:* @@ -1321,8 +1321,8 @@ importers: specifier: ^0.7.3 version: 0.7.3 turbo-stream: - specifier: 2.3.0 - version: 2.3.0 + specifier: 2.4.0 + version: 2.4.0 devDependencies: '@types/set-cookie-parser': specifier: ^2.4.1 @@ -14317,8 +14317,8 @@ packages: safe-buffer: 5.2.1 dev: false - /turbo-stream@2.3.0: - resolution: {integrity: sha512-PhEr9mdexoVv+rJkQ3c8TjrN3DUghX37GNJkSMksoPR4KrXIPnM2MnqRt07sViIqX9IdlhrgtTSyjoVOASq6cg==} + /turbo-stream@2.4.0: + resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} dev: false /tweetnacl@0.14.5: