remix-run / remix

Build Better Websites. Create modern, resilient user experiences with web fundamentals.
https://remix.run
MIT License
29.23k stars 2.46k forks source link

useLoaderData types broken if unstable_data is conditionally used #9826

Open michaelgmcd opened 1 month ago

michaelgmcd commented 1 month ago

Reproduction

With the following loader:

export const loader = unstable_defineLoader(async ({ request }) => {
  const isBot = isbot(request.headers.get('user-agent'));

  // Prevents making a session for every bot request
  if (isBot) {
    return { notification: null };
  }

  const session = await sessionStorage.getSession(request.headers.get('Cookie'));
  const notification = session.get('notification') ?? null;

  return unstable_data({ notification }, { 'Set-Cookie': await sessionStorage.commitSession(session) });
});

const { notification } = useLoaderData<typeof loader>(); returns a type issue. The issue exists because of the conditional use of unstable_data. If I remove the "isBot" block, the type issue is gone.

Screenshot 2024-08-04 at 9 01 46 PM

System Info

System:
    OS: macOS 14.5
    CPU: (10) arm64 Apple M2 Pro
    Memory: 2.85 GB / 32.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.16.0 - /opt/n/bin/node
    npm: 10.8.1 - /opt/n/bin/npm
    pnpm: 9.4.0 - /opt/n/bin/pnpm
    Watchman: 2024.06.17.00 - /opt/homebrew/bin/watchman
  Browsers:
    Chrome: 127.0.6533.89
    Safari: 17.5

Used Package Manager

pnpm

Expected Behavior

No type issue. In the use case above, notification would be NotificationType | null

Actual Behavior

Type issue attached above.

michaelgmcd commented 1 month ago

The following works for us. Happy to make a PR. "Unwrap" naming could be improved.

type Serializable =
    | undefined
    | null
    | boolean
    | string
    | symbol
    | number
    | Serializable[]
    | { [key: PropertyKey]: Serializable }
    | bigint
    | Date
    | URL
    | RegExp
    | Error
    | Map<Serializable, Serializable>
    | Set<Serializable>
    | Promise<Serializable>;

  type DataFunctionReturnValue =
    | Serializable
    | DataWithResponseInit<Serializable>
    | TypedDeferredData<Record<string, unknown>>
    | TypedResponse<Record<string, unknown>>;

  export type Unwrap<T extends DataFunctionReturnValue> =
    T extends TypedDeferredData<infer D>
      ? D
      : T extends TypedResponse<Record<string, unknown>>
        ? SerializeFrom<T>
        : T extends DataWithResponseInit<infer D>
          ? D
          : T;

  export type Serialize<T extends Loader | Action> =
    Awaited<ReturnType<T>> extends DataFunctionReturnValue
      ? Unwrap<Awaited<ReturnType<T>>>
      : Awaited<ReturnType<T>>;