remix icon indicating copy to clipboard operation
remix copied to clipboard

useLoaderData type inference broken when returned data has a `data` key

Open dmarkow opened this issue 2 years ago • 2 comments

What version of Remix are you using?

1.11.1

Steps to Reproduce

If the object returned from the loader includes a data key, the type inference returns only the items nested under that key. So in this case, typescript thinks useLoaderData returns just {item: number}. The code works just fine - item and other are being destructured properly and displaying on the page, it's just the types that are wrong. If I rename the data key to something else, it works fine and typescript returns the full type, e.g. {myData: {item: number}, other: number}. This started when defer was released.

export const loader = async () => {
  return json({
    data: { item: 1 },
    other: 2
  })
}

export default function Index() {
  const { data: { item }, other } = useLoaderData<typeof loader>();

  return <div>Item: {item}, Other: {other}</div>;
}
> yarn tsc
yarn run v1.22.19
$ /Users/dylan/dev/data-issue/node_modules/.bin/tsc
app/routes/index.tsx:12:11 - error TS2339: Property 'data' does not exist on type 'SerializeDeferred<{ item: number; }>'.

12   const { data: { item }, other } = useLoaderData<typeof loader>();
             ~~~~

app/routes/index.tsx:12:27 - error TS2339: Property 'other' does not exist on type 'SerializeDeferred<{ item: number; }>'.

12   const { data: { item }, other } = useLoaderData<typeof loader>();
                             ~~~~~

Expected Behavior

Type inference should work as it did before, or if data is a reserved name, the documentation should say as much.

Actual Behavior

Typescript infers the wrong types, but the code works fine.

dmarkow avatar Jan 22 '23 19:01 dmarkow

I've this problem as well with the latest version of Remix. I just wanted to add that it also happens when using useFetcher in actions.

Reproduced this in a minimal repository for you: https://github.com/emilbryggare/remix-types/

import type { LoaderArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export const loader = async ({ request }: LoaderArgs) => {
  // Generate correct types.
  // return json({ myData: { message: "Hello World!" } });

  // Does not generate correct types, e.g. SerializeDeferred
  return json({ data: { message: "Hello World!" } });
};
export default function Index() {
  let loaderData = useLoaderData<typeof loader>();
  // Type is when data key is present in return object:
  //   SerializeDeferred<{
  //     message: string;
  // }>
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
      <h1>Welcome to Remix</h1>
    </div>
  );
}

emilbryggare avatar Jan 25 '23 10:01 emilbryggare

Looks like this is the problem. https://github.com/remix-run/remix/blob/006b58bfec02622f9881c1e37444baef60584eda/packages/remix-server-runtime/serialize.ts#L21-L31

https://github.com/remix-run/remix/blob/006b58bfec02622f9881c1e37444baef60584eda/packages/remix-server-runtime/responses.ts#L9-L14

TypedDeferredData<infer U> matches on our result because there is a data key, instead of moving down to SerializeObject which it should since I'm not actually using defer.

dmarkow avatar Jan 25 '23 16:01 dmarkow

Can confirm this is also a problem with actions, returning POJOs or using the json helper.

CapitaineToinon avatar Jan 30 '23 21:01 CapitaineToinon

This is still an issue with actions, typed loaders are working for me.

Meaning, useActionData<typeof action>() is inferred completely wrong.

moishinetzer avatar Feb 07 '23 02:02 moishinetzer

To help solve the issue, it comes from this :

declare type Serialize<T> = IsAny<T> extends true ? any : T extends TypedDeferredData<infer U> ? SerializeDeferred<U> : ...

export declare type TypedDeferredData<Data extends Record<string, unknown>> = Pick<DeferredData, "init"> & {
    data: Data;
};

Type narrowing tends to consider every payload with {data: ..} to be a "defer" value.

balzdur avatar Feb 14 '23 14:02 balzdur

Same issue here:

image

ronnylt avatar Feb 16 '23 07:02 ronnylt

This problem still exists ! Any idea how to fix it?

raminrez avatar Feb 21 '23 09:02 raminrez

Thanks for reporting the issue! I posted it on the Discord a couple of weeks ago and was late to report it here :)

For debbuging without setting up a whole environment, I would recommend the typescript sandbox. Here is a gist of the issue on TS Playground.

As you can see, the type inference takes what is in the "data" property instead of considering the whole object.

Another example:

export async function action({ request }: ActionArgs) {
  // Other returns are possible
  return json({ data: { foo: "bar" }, errors: { foo: "baz" })
}

export default function Something() {
  const actionData = useActionData<typeof action>()
  //    ^ actionData has type { foo: "bar" } 🤷‍♂️
  const data = actionData && "data" in actionData ? actionData.data : null
  //    ^ ❌ data is of type string, but should be { foo: string }
  //          But it works if the property is named something else
  //          than "data", like "formData" 🤷‍♂️
}

michaeldebetaz avatar Feb 22 '23 07:02 michaeldebetaz

That also happened to me using Remix 1.14.0 (cloudflare runtime).

okalil avatar Mar 18 '23 11:03 okalil

This is still an issue with actions, typed loaders are working for me.

Meaning, useActionData<typeof action>() is inferred completely wrong.

To solve this issue, I implement return json(data) in the "action" function.

huyphamfc avatar Mar 22 '23 04:03 huyphamfc

This still occurs on Remix 1.15, and it also occurs on the SerializeFrom type helper, and the useFetcher<Type>() generic type.

I also have a typescript playground for testing.

mikeybinns avatar Apr 04 '23 11:04 mikeybinns

here is a code sandbox replication as well

https://codesandbox.io/p/sandbox/confident-feather-x0oe5q?file=%2Fapp%2Froot.tsx&selection=%5B%7B%22endColumn%22%3A21%2C%22endLineNumber%22%3A31%2C%22startColumn%22%3A21%2C%22startLineNumber%22%3A31%7D%5D

bryce-pearce avatar Apr 08 '23 01:04 bryce-pearce

Closed by #5516

MichaelDeBoey avatar Apr 11 '23 22:04 MichaelDeBoey

🤖 Hello there,

We just published version v0.0.0-nightly-9af8868-20230412 which involves this issue. If you'd like to take it for a test run please try it out and let us know what you think!

Thanks!

github-actions[bot] avatar Apr 12 '23 07:04 github-actions[bot]

It looks like it resolved the issue for us from what I can see.

jcloutier-indeed avatar Apr 12 '23 14:04 jcloutier-indeed

1.16.0 is out 🎉

machour avatar May 01 '23 22:05 machour

What version of Remix are you using?

1.11.1

Steps to Reproduce

If the object returned from the loader includes a data key, the type inference returns only the items nested under that key. So in this case, typescript thinks useLoaderData returns just {item: number}. The code works just fine - item and other are being destructured properly and displaying on the page, it's just the types that are wrong. If I rename the data key to something else, it works fine and typescript returns the full type, e.g. {myData: {item: number}, other: number}. This started when defer was released.

export const loader = async () => {
  return json({
    data: { item: 1 },
    other: 2
  })
}

export default function Index() {
  const { data: { item }, other } = useLoaderData<typeof loader>();

  return <div>Item: {item}, Other: {other}</div>;
}
> yarn tsc
yarn run v1.22.19
$ /Users/dylan/dev/data-issue/node_modules/.bin/tsc
app/routes/index.tsx:12:11 - error TS2339: Property 'data' does not exist on type 'SerializeDeferred<{ item: number; }>'.

12   const { data: { item }, other } = useLoaderData<typeof loader>();
             ~~~~

app/routes/index.tsx:12:27 - error TS2339: Property 'other' does not exist on type 'SerializeDeferred<{ item: number; }>'.

12   const { data: { item }, other } = useLoaderData<typeof loader>();
                             ~~~~~

Expected Behavior

Type inference should work as it did before, or if data is a reserved name, the documentation should say as much.

Actual Behavior

Typescript infers the wrong types, but the code works fine.

I face same issue but in my project I use JavaScript instead of TypeScript

I use Remix 2.0.0 version Error: Property 'filterData' does not exist on type 'unknown' Image (https://github.com/remix-run/remix/assets/96987506/f7753cfe-d0bb-4d2a-93c7-504188133d1c)

Darshan562 avatar Dec 20 '23 07:12 Darshan562

@Darshan562 a bunch of types were improved in 2.1-2.4. If you still have issues after upgrading to those versions, please file a separate issue.

pcattori avatar Dec 21 '23 00:12 pcattori