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

ClientResponse<T>.json() returns never when T is a union type of structurally different objects #1783

Closed
moreirathomas opened this issue Dec 5, 2023 · 1 comment · Fixed by #1786
Labels

Comments

@moreirathomas
Copy link

moreirathomas commented Dec 5, 2023

What version of Hono are you using?

3.11.2

What runtime/platform is your app running on?

Found on Bun (v1.0.15) initially, reproduced on Deno (v1.38.4)

What steps can reproduce the bug?

Hi, thanks for building Hono!

I ran into an unexpected behavior when trying to use ClientResponse.json(). In the following minimal reproduction, the type information is lost, we get never.

import { hc, Hono } from "https://deno.land/x/hono@v3.11.2/mod.ts";

type ErrorResponse = {
  error: Error;
};

type OkResponse = {
  data: string;
};

// Simulate business logic.
declare function condition(): Promise<boolean>;

const app = new Hono();

const api = app.get("/", async (c) => {
  const ok = await condition();
  // Expect response type to be TypedResponse<OkResponse | ErrorResponse>
  if (ok) {
    return c.json<OkResponse>({ data: "foo" });
  }
  return c.json<ErrorResponse>({ error: new Error("error") });
});

const client = hc<typeof api>("http://localhost:8000");

const res = await client.index.$get();

const json = await res.json();

What is the expected behavior?

I expected the type to be OkResponse | ErrorResponse.

Explicitly typing the handler 's return value does not help.

const api = app.get("/", async (c): Promise<TypedResponse<OkResponse | ErrorResponse>> => {
  // ...
});

What do you see instead?

Capture d’écran 2023-12-05 à 21 22 45

Additional information

It seems that the type breaks only on union of object (record) types:

declare function condition(): boolean;

const route = app
  .get("/primitive|primitve", (c) => {
    return condition() ? c.json(1) : c.json("a");
  })
  .get("/primitive|primitive[]", (c) => {
    return condition() ? c.json([1, 2, 3]) : c.json("a");
  })
  .get("/primitive[]|primitive[]", (c) => {
    return condition() ? c.json([1, 2, 3]) : c.json(["a", "b", "c"]);
  })
  .get("/object[]|primitive[]", (c) => {
    return condition() ? c.json([{ k: true }]) : c.json(["a", "b", "c"]);
  })
  .get("/object[]|object[]", (c) => {
    return condition() ? c.json([{ k: true }]) : c.json([{ l: 0 }]);
  })
  .get("/object|object", (c) => {
    return condition() ? c.json({ k: true }) : c.json({ l: true });
  });

const client = hc<typeof route>("http://localhost:8000");

const a = await client["primitive|primitve"].$get().then((res) => res.json());
//    ^ string | number
const b = await client["primitive|primitive[]"].$get().then((res) => res.json());
//    ^ string | number[]
const c = await client["primitive[]|primitive[]"].$get().then((res) => res.json());
//    ^ string[] | number[]
const d = await client["object[]|primitive[]"].$get().then((res) => res.json());
//    ^ { k: boolean }[] | string[]
const e = await client["object[]|object[]"].$get().then((res) => res.json());
//    ^ { k: boolean }[] | { l: number }[]
const f = await client["object|object"].$get().then((res) => res.json());
//    ^ never

I suspect it has something to do with the signature of ClientResponse.json(): Promise<BlankRecordToNever<T>>.

type BlankRecordToNever<T> = keyof T extends never ? never : T

type X = BlankRecordToNever<{ a: string } | { b: string }>;
//   ^ never (as expected)

Is this an expected behavior?

@yusukebe
Copy link
Member

yusukebe commented Dec 7, 2023

Hi @moreirathomas

Thanks for raising the issue. it might be fixed in latest release v3.11.3. Check git it a try!

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

Successfully merging a pull request may close this issue.

2 participants