hono icon indicating copy to clipboard operation
hono copied to clipboard

Return type of `get` method on `Hono`'s subclass is `Hono`, not the subclass

Open yudai-nkt opened this issue 1 year ago • 9 comments

What version of Hono are you using?

3.7.3

What runtime/platform is your app running on?

N/A (my issue is about typing, not a runtime problem)

What steps can reproduce the bug?

When you define a class extending Hono and call a get method (or other similar ones), the return value is inferred to be Hono instead of the subclass.

Below is a reproduction code and the corresponding playground link:

import { Hono } from "hono";

class MyCustomHono extends Hono {
  constructor(init?: ConstructorParameters<typeof Hono>[0]) {
    super(init);
  }
  customMethod() {
    return this;
  }
}

const foo = new Hono().get("/", (c) => c.text("Hello!"));
//    ^? const foo: Hono<Env, ToSchema<"get", "/", unknown, {}>, "/">;
const bar = new MyCustomHono().get("/", (c) => c.text("Hello subclass!"));
//    ^? const bar: Hono<Env, ToSchema<"get", "/", unknown, {}>, "/">;
const baz = new MyCustomHono().customMethod();
//    ^? const baz: MyCustomHono

What is the expected behavior?

bar is inferred to be MyCustomHono.

What do you see instead?

bar is inferred to be Hono.

Additional information

A real world example of this issue would be @hono/zod-openapi and I actually found this behavior in that situation.

yudai-nkt avatar Oct 02 '23 03:10 yudai-nkt

Hi @yudai-nkt

This should be fixed. I think it is because the return value of get is specified as Hono as follows. I can't think of a solution right now, but I will look into it.

https://github.com/honojs/hono/blob/82f103302bf07c7eb54f124768f45677e5efa41b/src/types.ts#L183

yusukebe avatar Oct 02 '23 04:10 yusukebe

Hmmmm. Fixing this issue is super difficult or imposible.

To achieve this, we must be able to "infer the type of a subclass in the parent class Hono and pass the types as a generic to it".

This means we must be able to this:

class Foo<Path extends string> {
  route<Path extends string>(path: Path): Foo<Path> { // This can be changed as you like.
    return this
  }
}

const main = new Foo()
const mainRoute = main.route('/abc') // Foo<"/abc"> - OK

class SubFoo<Path extends string> extends Foo<Path> {}
const sub = new SubFoo()
const subRoute = sub.route('/abc') // Foo<"/abc"> - NG, should be SubFoo<"/abc">

Playground.

  • The type of mainRoute should be Foo<"/abc">, not Foo<string>. The subRoute must be SubFoo<"/abc"> as well.
  • We don't write new Foo<'/abc'>.
  • You can change the return type of route in Foo class, currently Foo<Path>.

@yudai-nkt @usualoma @Code-Hex @sor4chi and others: Is there a superman who can solve this?

yusukebe avatar Oct 02 '23 16:10 yusukebe

The zod-openapi package in the middleware repo actually has the exact same problem. I don't think there is any good way to accomplish this in typescript, though

ZerNico avatar Oct 02 '23 19:10 ZerNico

Yeah.

Actually, before creating "Zod OpenAPI", I had not really thought about extending Hono to make a sub class. For Hono's flexible type inference, the return type of each method must be limited to Hono maybe.

I may have forgotten to ping you, but can you solve this problem? @usualoma @Code-Hex @masnormen @NicoPlyley

yusukebe avatar Oct 03 '23 00:10 yusukebe

@yusukebe

I believe this is not possible, but it's not a limitation of TypeScript but the intended way of doing it.

My understanding of is it that when calling a method on a subclass, the inferred return type defaults to that of the parent class. This provides as a way to indicate that methods from the parent class are also valid on the subclass.

This ensures that a subclass can be used interchangeably with its parent class without causing issues, and any overridden returns follow the types of the parent class following Liskov Substitution Principle

NicoPlyley avatar Oct 03 '23 08:10 NicoPlyley

@NicoPlyley Thanks for explaining! I understood it well!

So, I hope we can solve real world problems.

@yudai-nkt @ZerNico Is there any specific problem with the subclass type being Hono?


I don't think @yudai-nkt has much of a practical problem with this repository.

https://github.com/yudai-nkt/hono-zod-openapi-html#possible-contributions-to-the-ecosystem

  • Cannot infer the correct type for endpoints that may return several response status
  • Returning a 204 response yields a compile error

As for these two issues, you should use c.jsonT() instead of c.body().

OpenAPIHono is not chainable after calling the doc method because it returns void

This problem can be fixed.

yusukebe avatar Oct 03 '23 09:10 yusukebe

Thanks everyone for tackling this!

Is there any specific problem with the subclass type being Hono?

Not really. We can work around this by swapping the route registration order.

  • Cannot infer the correct type for endpoints that may return several response status
  • Returning a 204 response yields a compile error

As for these two issues, you should use c.jsonT() instead of c.body().

I tried but jsonT didn't help unfortunately. I don't want to discuss further here since it's off-topic, but I prepared a reproduction.

yudai-nkt avatar Oct 03 '23 14:10 yudai-nkt

@yudai-nkt

Thanks for your response. Okay, If we can work around it, let's do it.

PS.

You can write jsonT({}) to avod the error. This is because null does not match the expected type {}. This is confusing and needs to be documented.

Playground

yusukebe avatar Oct 03 '23 14:10 yusukebe

To achieve this, we must be able to "infer the type of a subclass in the parent class Hono and pass the types as a generic to it".

Correct me if I'm wrong, but could the subclass just override each of the methods (get, post, etc) with the corrected type?

ibash avatar Apr 10 '24 07:04 ibash

As mentioned above, it is impossible not to treat the Hono's subclass as Hono. Closing this issue. Thanks!

yusukebe avatar Jun 06 '24 08:06 yusukebe