[go: up one dir, main page]

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

Type mismatch when returning from useLoaderData #8874

Open
ananevam opened this issue Feb 24, 2024 · 9 comments
Open

Type mismatch when returning from useLoaderData #8874

ananevam opened this issue Feb 24, 2024 · 9 comments

Comments

@ananevam
Copy link

Reproduction

When strictNullChecks is disabled, if all fields within a field are optional, then the parent field itself becomes optional. This is due to wrapping the returned result in a JsonifyObject.
Sorry for my bad English.

type Result = {
    foo: {bar?: string}
};

export const loader = () => {
    const foo: Result = {foo: {bar: '123'}}
    return json(foo)
}

export default function Component() {
    const data = useLoaderData<typeof loader>()
    const foo: Result = data;
}
Type 'JsonifyObject<Result>' is not assignable to type 'Result'.
  Property 'foo' is optional in type 'JsonifyObject<Result>' but required in type 'Result'.

playground

System Info

-

Used Package Manager

npm

Expected Behavior

types are the same

Actual Behavior

types don't match

@iampeter
Copy link

Experiencing the same issues.

@littlejustinh
Copy link

i generally have to do return json<Result>(...) to get types to work right

@kiliman
Copy link
Collaborator
kiliman commented Feb 27, 2024

It's one of the reasons I created remix-typedjson. It exposes the native TypeScript types and converts the JSON data back to native types automatically.

https://github.com/kiliman/remix-typedjson

@iampeter
Copy link

@kiliman great that you've created the repo, I'll check it out.

But I think this is a core Remix issue, isn't it?

@kiliman
Copy link
Collaborator
kiliman commented Feb 28, 2024

@iampeter I think any framework that serializes to JSON from server to client will have this issue. I believe once Remix supports RSC, they'll be able to stream native types to the client, and this will no longer be an issue.

@ananevam
Copy link
Author
ananevam commented Feb 28, 2024

@iampeter I think any framework that serializes to JSON from server to client will have this issue. I believe once Remix supports RSC, they'll be able to stream native types to the client, and this will no longer be an issue.

we are not talking about serialization of types, but about what type useLoaderData returns.

it works correctly

type ExtractGeneric<Type> = Type extends TypedResponse<infer X> ? X : never
export function useDataFromLoader<T extends LoaderFunction>() {
  return useLoaderData() as ExtractGeneric<Awaited<ReturnType<T>>>
}

playground

@Piotrovskyi
Copy link
import { useLoaderData } from "@remix-run/react"

export const useCustomLoaderData = <T extends (...args: any) => any>() => {
  return useLoaderData() as Awaited<ReturnType<T>>
}

@Magellol
Copy link
Magellol commented Sep 29, 2024

I've noticed a related issue

declare const x: JsonifyObject<{
  foo: unknown;
}>;

// foo?: unknown
x.foo

x.foo turned optional for some reason. It doesn't happen for something like string for instance.

declare const x: JsonifyObject<{
  foo: string;
}>;

// foo: string
x.foo

@akomm
Copy link
akomm commented Oct 17, 2024

I'd like to drop an explanation why this happens for those that don't understand. It might help working around or fixing the issue.

While you can return any expression from a loader, when its transported as JSON, many values get converted to string or removed, because they are not supported by JSON. In order to address it, the type JsonifyObject has been created, that maps the returned type to what you would receive on the client side.

Here a demo of the issue: TS Playground

@ananevam you've mentioned, that the type comes from useLoaderData:
The data comes from your loader, the type is inferred from what your loader returns via the useLoaderData type. This is why you pass typeof loader as generic to your useLoaderData. The problem starts at the server, where the data is serialized. If remix did not map the type to reflect JSON.stringify behavior, you would see and expect incorrect types on the client side.

So there are currently a couple of ways to solve the issue (they are all not optimal).

  1. Prevent returning any types, like undefined, that are expected in your components from loader, because they get changed
  2. Widen types in your components to null | undefined
  3. Use a data transformer, like for example the one provided by @kiliman

As @kiliman mentioned, in the next BIG remix update we will be able to transfer data more complex than simple JSON and you won't have the issue.

But meanwhile... We need to bite the dust. I'm also experience the issue and its annoying.

@Magellol I can explain, why unknown gets modified this way. Because unknown includes undefined and when you JSON.stringify it, it would disappear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants