Tech Note: Type inference takes place when initializing variables and members
Tags: typescript, hono, webdev, technote
Published: 2024-05-14 20:24:28

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hwvtgy2mu8z9a40ojyw1.png)
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](https://hono.dev/)

## 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](https://hono.dev/guides/rpc)

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.

```ts
// 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 };
```

### Step2: Exporting the API type from server

```ts
// 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;
```

### Step3: Configuring the paths in client side

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

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

    /// ...
  },
  "include": ["src"]
}
```

```ts
// 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'),
    },
  },
});
```

### Step4: Importing the API type in the client

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

```ts
// 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);
```

## 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 👎

```ts
const expensesRoute = new Hono();

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

export { expensesRoute };
```

I should have defined the routes **declaratively**, using method chain, as It's suggested in [Documentation](https://hono.dev/guides/rpc#server)

### Good 👍

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

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1o1ao5j76sh0s0l9liz7.png)

### Why it happened?

According to the [TypeScript Documentation](https://www.typescriptlang.org/docs/handbook/type-inference.html), 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.