DEV Community

kanta1207
kanta1207

Posted on

Tech Note: Type inference takes place when initializing variables and members

This article is basically a tech note reflecting the things I got stack with.

What I got stuck with

When I was trying to use RPC for the first time with Hono, my hono client instance kept being inferred as type unknown, even though I was sure that I was exporting the correct type from the server.

Image description
I'll leave brief information about Hono & RPC.

What is Hono?

Hono is a web framework mainly for building APIs with TypeScript. It is designed to be simple,fast, and easy to use.
It's reckon as a modern alternative to Express.js.

Officail documentation

What is RPC?

RPC stands for Remote Procedure Call.
It is a protocol that one program can use to request a service from a program located in another computer in a network without having to understand the network's details.

In the context of web development, it is a way to call a function on a remote server as if it were a local function.
To be more specific & frank in here, it's a way to share API specifications between the server and the client.

RPC documentation in Hono

Here's step by step summary of what I did.

Step1: Expenses route for sample app.

I wrote a simple CRUD API for Expenses in the server side with Hono, as I used to do with Express.

// server/routes/expenses.routes.ts
import { zValidator } from '@hono/zod-validator';
import { Hono } from 'hono';
import z from 'zod';

const expenseSchema = z.object({
  id: z.number().int().positive().min(1),
  name: z.string().min(3).max(255),
  amount: z.number(),
});

type Expense = z.infer<typeof expenseSchema>;

const expenses: Expense[] = [
  { id: 1, name: 'Rent', amount: 1000 },
  { id: 2, name: 'Food', amount: 200 },
  { id: 3, name: 'Internet', amount: 50 },
];

const expensesRoute = new Hono();

expensesRoute.get('/', (c) => {
  return c.json({ expenses: [] });
});

expensesRoute.post('/', zValidator('form', createPostSchema), async (c) => {
  const expense = await c.req.json();

  expenses.push(expense);
  return c.json(expense);
});

expensesRoute.get('/:id', (c) => {
  const id = parseInt(c.req.param('id'));
  const expense = expenses.find((e) => e.id === id);

  if (!expense) {
    return c.notFound();
  }

  return c.json(expense);
});

expensesRoute.delete('/:id', (c) => {
  const id = parseInt(c.req.param('id'));
  const expense = expenses.find((e) => e.id === id);

  if (!expense) {
    return c.notFound();
  }

  const index = expenses.indexOf(expense);
  const deletedExpense = expenses.splice(index, 1);

  return c.json(deletedExpense);
});

export { expensesRoute };
Enter fullscreen mode Exit fullscreen mode

Step2: Exporting the API type from server

// sever/index.ts
import { Hono } from 'hono';
import { expensesRoute } from './routes/expenses.routes';
// ...
const app = new Hono();
// ...
const apiRoutes = app.route('/expenses', expensesRoute);

export default app;
export type ApiRoutes = typeof apiRoutes;
Enter fullscreen mode Exit fullscreen mode

Step3: Configuring the paths in client side

// client/tsconfig.json
{
  "compilerOptions": {
   /// ...

    "paths": {
      "@/*": ["./src/*"],
      "@server/*": ["../server/*"]
    },

    /// ...
  },
  "include": ["src"]
}
Enter fullscreen mode Exit fullscreen mode
// client/vite.config.ts
import path from 'path';
import react from '@vitejs/plugin-react';

import { defineConfig } from 'vite';

export default defineConfig({
  /// ...
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@server': path.resolve(__dirname, '../server'),
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Step4: Importing the API type in the client

In this step, I found out that hono client instance was inferred as type unknown.

// client/src/lib/api.ts
// I configured the
import { type ApiRoutes } from '@server/src';
import { hc } from 'hono/client';

const API_URL: string = import.meta.env.DEV
  ? 'http://localhost:8787'
  : import.meta.env.VITE_API_URL;

const client = hc<ApiRoutes>(API_URL);
Enter fullscreen mode Exit fullscreen mode

The Problem & Solution

The problem was in the step 1.
I defined each route indiviually and imperatively , like I used to do with Express.js.
The API itself was working fine with this way, but the type wasn't inferred correctly.

Bad đź‘Ž

const expensesRoute = new Hono();

expensesRoute.get('/', (c) => { ... });
expensesRoute.post('/', zValidator('form', createPostSchema), async (c) => { ... });
expensesRoute.get('/:id', (c) => { ... });
expensesRoute.delete('/:id', (c) => { ... });

export { expensesRoute };
Enter fullscreen mode Exit fullscreen mode

I should have defined the routes declaratively, using method chain, as It's suggested in Documentation

Good đź‘Ť

export const expensesRoute = new Hono()
  .get('/', (c) => { ... })
  .post('/', zValidator('form', createPostSchema), async (c) => { ... })
  .get('/total-spent', async (c) => { ... })
  .get('/:id', (c) => { ... })
  .delete('/:id', (c) => { ... });
Enter fullscreen mode Exit fullscreen mode

After fixing this, the hono client instance was correcly inferred with all of the route definition.

Image description

Why it happened?

According to the TypeScript Documentation, type inference takes place when initializing variables and members, setting parameter default values, and determining function return types.
So when I defined the routes imperatively, and added it to expensesRoute, TypeScript couldn't infer each of the type correctly. This is probably why the hono client instance was inferred as type unknown.

Takeaways

  • Declarative over Imperative: When defining routes in Hono, it's better to use method chain to define the routes declaratively.

  • Type Inference: TypeScript infers types when initializing variables and members, setting parameter default values, and determining function return types. So, if you're having trouble with type inference, check if you're defining the types correctly in these places.

Top comments (0)