trpc / trpc

๐Ÿง™โ€โ™€๏ธ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.
https://tRPC.io
MIT License
33.55k stars 1.19k forks source link

bug: SerializeObject<UndefinedToOptional<X>> is incompatible #3307

Closed abeauchemin-planned closed 1 year ago

abeauchemin-planned commented 1 year ago

Provide environment information

  System:
    OS: macOS 12.2.1
    CPU: (10) arm64 Apple M1 Pro
    Memory: 96.77 MB / 16.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 16.18.0 - ~/.nvm/versions/node/v16.18.0/bin/node
    npm: 8.19.2 - ~/.nvm/versions/node/v16.18.0/bin/npm
  Browsers:
    Brave Browser: 103.1.41.100
    Chrome: 107.0.5304.110
    Safari: 15.3
  npmPackages:
    @tanstack/react-query: ^4.18.0 => 4.18.0 
    @trpc/client: ^10.4.1 => 10.4.1 
    @trpc/react-query: ^10.4.1 => 10.4.1 
    @trpc/server: ^10.4.1 => 10.4.1 
    next: ^12.0.8 => 12.0.8 
    react: ^17.0.2 => 17.0.2 
    typescript: ^4.5.4 => 4.5.4 

Describe the bug

I was on v10.0.0-rc.8 and upgraded to latest (as of now) v10.4.1 and suddenly got type errors on some frontend(react) calls. On the server, the return type of the endpoint is a User interface generated by zod

When retrieving the data using useQuery, there's an error when I pass down the data returned by the endpoint to a child component expected that same User interface

  const { data: user, error, isLoading } = trpc.user.getByEmail.useQuery(email)
  ...
  <SomeComponent user={user} ... /> 

result in

Type 'SerializeObject<UndefinedToOptional<{ source?: "organic" | "organic-invite" | "organic-invite-pending" | "admin-invite"; title?: userTitles; _uui
d?: string; created_by?: string; invited_by?: unknown; isVerified?: boolean; ... 12 more ...; ssoSignup: boolean; }>>' is not assignable to type '{ source?: "organic" | "organic-invite" | "organic-invite-pending" | "ad
min-invite"; title?: userTitles; _uuid?: string; created_by?: string; invited_by?: unknown; isVerified?: boolean; ... 12 more ...; ssoSignup: boolean; }'.
  Types of property 'nameInfo' are incompatible.
    Type 'SerializeObject<UndefinedToOptional<{ firstName: string; lastName: string; }>>' is not assignable to type '{ firstName: string; lastName: string; }'.
      Property 'firstName' is optional in type 'SerializeObject<UndefinedToOptional<{ firstName: string; lastName: string; }>>' but required in type '{ firstName: string; lastName: string; }'.

47         <MyChildComponent user={user} />

image

I'm assuming this was introduced in 10.2.0 maybe?

Link to reproduction

https://stackblitz.com/edit/github-4mkvan?file=src/pages/index.tsx

Edit I was able to reproduce on stackblitz but now after a small cleanup the error is gone and I can't reproduce :/ Trying to reproduce again

To reproduce

The user passed down to the child component triggers a typescript error in src/pages/index.tsx in the provided example

Additional information

No response

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributing

abeauchemin-planned commented 1 year ago

For now it can be fixed by using v10.1.0

KATT commented 1 year ago

Are you using any of the inference helpers in your app?

We can't fix this without a reproduction

abeauchemin-planned commented 1 year ago

Thanks for your response. Nope, not using the inference helpers AFAIK. I'm looking for a way to give you a reproducible repo. It happened to all our devs so far so there's definitely something going on in our repo, unfortunately it's private but I'll try to extract only the relevant part in something I can share

StireArLankar commented 1 year ago

Hello.

I have reproduction example. Basically direct usage of types like

type ExampleResponse1 = { optionalField: {} | undefined }

in both client and server (typing something in client part with it, instead of using inference helpers) breaks if use SerializeObject<UndefinedToOptional<T>> where T is expected.

In my case, i have generated gRPC types, where i want TS to tell me to put undefined so i always know what fields i use.

So solutions are:

abeauchemin-planned commented 1 year ago

Thanks @StireArLankar , looks like you're correct I've also been able to reproduce by using an interface that has an optional object Here's an example that should yield the error, using the User interface from zod or User2 which is the same type as what zod infers from the schema

// Backend

const appRouter = router({
  greeting: publicProcedure
    .input()
    .query(() => {
      return findUser();
    }),
});

const User = z.object({
  nameInfo: z.object({
    firstName: z.string(),
    lastName: z.string(),
  }).optional()
})

export type User = z.infer<typeof User>;

interface User2 {
  nameInfo?: {
    firstName: string
    lastName: string
  } | undefined
}

function findUser(): User {
  return {
    nameInfo: {
      firstName: 'first',
      lastName: 'last',
    },
  }
}

// Frontend

  const { data: user, error, isLoading } = trpc.greeting.useQuery()

  function doSomething(userData: User) {
    console.log(userData)
  }

  doSomething(user)

It looks like in that case SerializeObject<UndefinedToOptional<...> makes firstName and lastName optional when in reality it's nameInfo that is optional but firstName and lastName are required when the property is defined

The weird thing is that I can easily reproduce in my company repo, but can't reproduce in the stackblitz starter project, might be related to some version of zod or typescript maybe?

Our setup is hard to reproduce because we have 3 different repos

KATT commented 1 year ago

Does it work if you upgrade TypeScript? Can you share your tsconfig?

abeauchemin-planned commented 1 year ago

I updated to latest typescript in all my repos and nothing But now that you mention tsconfig, I think the issue might be that my backend repo has strict mode set to true and the frontend has "strict": false. So I'm guessing typescript might interpret the type differently because of strict mode

It's getting late here, I'll do more testing tomorrow but I really think it's related to having different strict mode values at both ends.

KATT commented 1 year ago

Alright, that seems likely.

As a rule of thumb tRPC doesn't support strict: false, we should probably add that to the docs

KATT commented 1 year ago

Will close for now. Feel free to reopen it if you can reproduce it.

abeauchemin-planned commented 1 year ago

Yes can confirm this is the case. Having strict true in both repos fix the issues. While I agree that every project should have strict mode, it might be a decent barrier of entry for people trying to migrate or use tPRC on an existing project

Turning strict mode on can be a colossal task, and looks like it was working pre v10.2.0, but it seems like the addition of SerializeObject<UndefinedToOptional<>> make this not feasible anymore

Maybe something to think about. Thanks to you both ๐Ÿ™

KATT commented 1 year ago

While I agree that every project should have strict mode, it might be a decent barrier of entry for people trying to migrate or use tPRC on an existing project

I agree but we can't cater to that. It's near-impossible to test & we aren't getting paid [much] to maintain tRPC.

abeauchemin-planned commented 1 year ago

Totally understandable Thanks a lot for all your hard work on this BTW, tRPC is amazing!