# The complete gRPC guide part 3: types everywhere with TypeScript!

In this chapter of our gRPC series, we're going to learn how to integrate TypeScript with gRPC and enjoy its full type system!

- URL: https://blog.lsantos.dev/en/the-complete-grpc-guide-part-3-types-everywhere-with-typescript/
- Published: 2021-05-26
- Updated: 2026-07-16
- Section: infra
- Series: grpc
- Tags: grpc, typescript, nodejs, series, protobuf, rest, architecture
- Language: en
- Author: Lucas Santos

---
In our [previous article of the guide](/o-guia-do-grpc-2/) we saw how we can integrate gRPC with JavaScript in a pretty simple and quick way. Now it's time to go up one more level and add types to this application! And when I say types, we automatically think of **TypeScript**!

For this article we're going to convert our [gRPC notes API](https://github.com/khaosdoctor/grpc-guide-part2-javascript-sample) to use TypeScript. But first, let's understand what "converting to TypeScript" means and what we mean when we do it with gRPC, especially when we use gRPC with JavaScript.

## What does "converting" mean?

As I already mentioned [in the first part of this guide](/guia-grpc-1/), gRPC, despite being an established technology, doesn't have great documentation or a great set of tools for some languages, and unfortunately one of those languages is [JavaScript](https://docs.microsoft.com/javascript/?WT.mc_id=opensource-0000-ludossan)...

That said, the tooling we have for [JS](https://docs.microsoft.com/javascript/?WT.mc_id=opensource-0000-ludossan) works really well, even though it's a bit complicated and a bit obscure to use. With a solid enough base, all we need to do is add type declarations to the files that are already generated by the compiler.

So, "converting" an application to TypeScript, in short, means we have to add `.d.ts` files to every `.js` file generated by the compiler. The task itself isn't very complicated, since we have [official libraries](https://github.com/Microsoft/dts-gen) that do this kind of thing. So we're all set, right?

Unfortunately, the problem we run into again comes back to the tooling we have for JavaScript. For example, we have two official packages, an older one called just `grpc`, which was the first gRPC implementation package for JavaScript, but this package is gradually being replaced by the `@grpc/grpc-js` package, which doesn't have the `.proto` file loader, making the package lighter and with fewer dependencies, since it outsources that functionality to another specific package called `@grpc/proto-loader`.

> We used both packages in the [previous article of this series](/o-guia-do-grpc-2/)

The differences between these packages go beyond one being just a variation of the other, or having fewer things bundled together. The reality is that the `grpc` package is better implemented because it's been with us for much longer, while, for example, the `@grpc/grpc-js` package is missing some implementations, like `grpc.Server.bind()`.

## Working with the differences

We already know these packages have differences, but is it possible to work with them? Yes, it's totally possible, but we have to be careful with some details that aren't very well documented and are, for the most part, the reason why I'm putting this guide together. Our JavaScript ecosystem for gRPC is extremely scattered and spread out across several levels, which makes implementing it in the language very complicated for beginners.

Since we had one package, `grpc`, for most of the time gRPC has existed, we started getting libraries built around it, like [Protobuf.js](https://github.com/protobufjs/protobuf.js), an official tool that basically handles creating files to wrap the message generation classes in gRPC, that is, we can generate classes for our messages and use them like this:

```js
const pbjs = require('protobuf.js')
pbjs.load('notes.proto', (err, root) => {
  if (err) throw err

  const NoteFindRequest = root.lookupType('NoteFindRequest')
  const payload = { id: 1 }

  const isValid = NoteFindRequest.verify(payload)
  if (!isValid) throw new Error(isValid)

  const message = NoteFindRequest.create(payload)
  const buffer = NoteFindRequest.encode(message).finish()
})
```

As you can see, we have a reflection model, that is, we have to do a lookup inside our `.proto` file, but it's also possible to generate files statically, so we'll have classes ready for this. On top of that, `protobuf.js` also includes the concept of `verify`, so we can check whether messages are correct, following a flow like this:

![](./toolset-a94729.svg)

And, on top of all that, it also generates TypeScript definitions, since `grpc` and `@grpc/grpc-js` ship with static typing alongside their packages. So we can basically do the same things but with our types defined.

Did you catch the problem we have here? Even though it's very good, this library doesn't have a good, simple way to generate self-contained TypeScript code, that is, code we can instantiate as a service instance without relying on dynamically loading a `.proto` file, meaning we have to use `.load` in our definition file and trust that it will be complete and valid, and in a statically typed superset like TypeScript, anything dynamic is a problem.

So, even though `protobuf.js` generates valid types, it's much better for when we're building a client than when we're building a server, since it provides much more tooling for messages than for inferring the server's internal types. But it can also be used as a [runtime type generator](https://github.com/protobufjs/protobuf.js#using-decorators) with decorators.

## What we want

Let's think about this a bit. We have a complete definition file, like this one:

```protobuf
syntax = "proto3";

service Notes {
  rpc List (Void) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

// Entities
message Note {
  int32 id = 1;
  string title = 2;
  string description = 3;
}

message Void {}

// Requests
message NoteFindRequest {
  int32 id = 1;
}

// Responses
message NoteFindResponse {
  Note note = 1;
}

message NoteListResponse {
  repeated Note notes = 1;
}
```

With this we already know basically everything we need to create our server. A gRPC server has two main interfaces: the first is the base interface containing all the methods that can be created, and the second is the server implementation. So, ideally, we'd be able to do something like this:All the types we're using in the call above, like `ServerUnaryCall` and `sendUnaryData`, [come natively from `@grpc/grpc-js`](https://github.com/grpc/grpc-node/blob/master/packages/grpc-js/src/server-call.ts#L84-L86), since the lib is written in TypeScript. See more of the types [in the official repository](https://github.com/grpc/grpc-node/blob/master/packages/grpc-js/src).

```typescript
class NotesServer implements NotesServerInterface {
  find (call: ServerUnaryCall<NoteFindRequest, NoteFindResponse>, callback: sendUnaryData<NoteFindResponse>) { /* implementation */ }

  list (_: ServerUnaryCall<Void, NoteListResponse>, callback: sendUnaryData<NoteListResponse>) { /* implementation */ }
}

const server = new grpc.Server()
server.addService(NotesService, new NotesServer())
```

The important part, besides the class's types, is the types we're going to pass to the server, so our `server.addService` call has a signature like this:

```typescript
abstract class Server {
  function addService (service: ServiceDefinition, implementation: UntypedServiceImplementation): void
}
```

And the `ServiceDefinition` and `UntypedServiceImplementation` types have, respectively, the following definitions:

```typescript
type ServiceDefinition<ImplementationType = UntypedServiceImplementation> = {
    readonly [index in keyof ImplementationType]: MethodDefinition<any, any>
}
```

See that it's not complex. What we have here is a `generic`: a service definition takes a type parameter, which methods are implemented; if we don't pass anything to it, we get an `UntypedServiceImplementation`, which basically means "I have no idea what's in here":

```typescript
export declare type UntypedHandleCall = HandleCall<any, any>
export interface UntypedServiceImplementation {
    [name: string]: UntypedHandleCall
}
```

Both types are index signatures that just define an object with a key of type string.

To sum it all up, what we have is an object that needs to have the same keys as its implementation, so `NotesService` needs to have the same keys as `NotesServer`, and the two types those keys refer to need to be method implementations, that is, functions with the following signature:

```typescript
export type MethodImplementation = (call: ServerUnaryCall<RequestInput, RequestOutput>, callback: sendUnaryData<RequestOutput>): void
```

We could even make it prettier by doing everything with generics:

```typescript
export type MethodImplementation<In, Out> = (call: ServerUnaryCall<In, Out>, callback: sendUnaryData<Out>): void
```

And all of this is just to show you that the types we need for gRPC aren't anything crazy, just objects that need to have the same keys as other objects and need to implement functions with a specific signature.

## Streams and other data types

A small aside, without direct relation to the content of this guide: if our call type isn't a `Unary` call, that is, if we have streaming of data on one of the sides, like, for example, in this `.proto`:

```protobuf
service Notes {
  rpc List (Void) returns (stream NoteListResponse);
  rpc Find (NoteFindRequest) returns (stream NoteFindResponse);
}
```

Then our responses would no longer be `ServerUnaryCall` or `sendUnaryData`, because we're no longer sending back a single type, but rather opening a data stream. Our `call` would be a `ReadableStream` and our callback would be a `WritableStream`, or essentially anything that implements `Readable` and `Writable` respectively.

We'll learn more about streaming with gRPC in the next parts of this guide.

## Getting to work

Now that we understand the details of TS types and how they're actually just indexes of one big object, let's move on to our implementation.

To convert our API to TypeScript, we're going to use a different library that's more specialized for `grpc-js`, since `protobuf.js` doesn't serve us well for this task. The repository for this example is available [on my GitHub](https://github.lsantos.dev/grpc-guide-part3-typescript-sample) if you want to take a look at the final code and the general structure.

The first thing we're going to do is a small change to our `.proto` file. The changes are minimal, we're just going to rename our `rpc` implementation to `Notes`

```protobuf
syntax = "proto3";

service Notes {
  rpc List (Void) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

// Entities
message Note {
  int32 id = 1;
  string title = 2;
  string description = 3;
}

message Void {}

// Requests
message NoteFindRequest {
  int32 id = 1;
}

// Responses
message NoteFindResponse {
  Note note = 1;
}

message NoteListResponse {
  repeated Note notes = 1;
}
```

This is just because if we called it `NoteService`, we'd end up with a weird name in our type, like `NoteServiceServer` (yes, I have OCD about variable names).

Let's create a new folder for our project, and inside it we'll create a `proto` folder and drop in the `notes.proto` file with the content we just saw. Next, we run `npm init -y` to create a new Node.js project and install the following packages:

```bash
$ npm i -D @types/long @types/node grpc_tools_node_protoc_ts grpc-tools typescript
```

As you can see, we're going to use the [`grpc-tools`](https://www.npmjs.com/package/grpc-tools) library, which is basically a wrapper around `protoc` with a few interesting tweaks, including generating `.d.ts` files. And we're going to use a plugin for this library called [`grpc_tools_node_protoc_ts`](https://github.com/agreatfool/grpc_tools_node_protoc_ts), which does the work of generating the types in a more structured way so we can create our services from a single interface.

These are our dev dependencies. For the runtime dependencies we're only going to have one, `grpc-js`, so let's run `npm i @grpc/grpc-js`. That's because we're generating static files that won't need to be read from a `.proto` file, so we don't need anything besides the server.

> If you want to generate files dynamically, you'll need the protobuf loader and, unfortunately, the generated types might not be ideal for you

Let's now start our TypeScript project with `npx tsc --init`. Once it's created, let's tweak the `tsconfig.json` variables so it's as strict as possible:

```json
//tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}
```

Now let's edit our `package.json` file to create our compile script. Let's create a script called `compile` which will basically compile our `.proto` file:

```bash
grpc_tools_node_protoc \
    --js_out=import_style=commonjs,binary:./proto \
    --grpc_out=grpc_js:./proto \
    -I ./proto ./proto/*.proto && \
  grpc_tools_node_protoc \
    --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
    --ts_out=grpc_js:./proto \
    -I ./proto ./proto/*.proto
```

This script will first generate the static JavaScript files, both the message type files (described by `js_out`) and the file that contains the ready-made server (described by `grpc_out`), all inside our `./proto` folder using the files that end in `*.proto`.

Right after that, we'll read the `.js` files generated in that folder and fire up the `protoc-gen-ts` plugin to generate their types and save them in the same folder.

> **Important**: notice we have a `ts_out=grpc_js` key in the second part of the command. That's because this library is prepared to generate types both for `grpc` and for `@grpc/grpc-js`

The last two scripts we're going to create are the scripts to start the server and the client, compiling from TS to JS first. In the end, we'll have a `scripts` key like this in our `package.json`:

```json
//package.json
{
  // ... content omitted
  "scripts": {
    "compile": "grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./proto --grpc_out=grpc_js:./proto -I ./proto ./proto/*.proto && grpc_tools_node_protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --ts_out=grpc_js:./proto -I ./proto ./proto/*.proto",
    "start:server": "tsc && node dist/server.js",
    "client": "tsc && node dist/client.js"
  },
  // ... content omitted
}
```

Run the `npm run compile` script and you'll see we get 4 new files in the `proto` folder:

```bash
proto
├── notes.proto # protobuf definition file
├── notes_grpc_pb.d.ts # gRPC server types
├── notes_grpc_pb.js # gRPC server
├── notes_pb.d.ts # message types
└── notes_pb.js # definition messages
```

## Where it all comes together

If you click on the `notes_grpc_pb.js` file, at the bottom you'll see it has a variable called `NotesService`:

```javascript
var NotesService = exports.NotesService = {
  list: {
    path: '/Notes/List',
    requestStream: false,
    responseStream: false,
    requestType: notes_pb.Void,
    responseType: notes_pb.NoteListResponse,
    requestSerialize: serialize_Void,
    requestDeserialize: deserialize_Void,
    responseSerialize: serialize_NoteListResponse,
    responseDeserialize: deserialize_NoteListResponse,
  },
  find: {
    path: '/Notes/Find',
    requestStream: false,
    responseStream: false,
    requestType: notes_pb.NoteFindRequest,
    responseType: notes_pb.NoteFindResponse,
    requestSerialize: serialize_NoteFindRequest,
    requestDeserialize: deserialize_NoteFindRequest,
    responseSerialize: serialize_NoteFindResponse,
    responseDeserialize: deserialize_NoteFindResponse,
  },
};
```

Compare this variable to the `ServiceDefinition` type we have in `grpc-js`. They're the same, meaning we can create a server directly from this constructor. Right below it we have a call to `grpc.makeGenericClientConstructor`, a method that generates a generic client for gRPC calls that we can use without needing an instance of our `.proto` file. We'll talk more about it in the next installments.

## Building the server

Let's move on to the interesting part now, where we build our server. Create a `src` folder and a `server.ts` file inside it.

Let's start by creating our "database". Remember it's an array of notes, but notes aren't plain objects anymore. Now we have types that define notes not only as objects but also as a message class, so the type we need to give this store is `Notes.AsObject[]`, that is, an array of objects that are valid `Notes` messages:

```typescript
import { Note } from '../proto/notes_pb'

const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]
```

Now let's implement our server. There are two ways to do this. The first is the object-based implementation, like we already did before, with an object like:

```js
const server = {
  find (call, cb) { ... },
  list (call, cb) { ... }
}
```

And the second is the class-based implementation, which ends up becoming an object after compilation anyway, but it's nicer and easier to understand when reading the code, so that's what we'll use. Let's start with a `NotesServer` class that implements an `INotesServer` interface generated by our `.proto` compiler:

```typescript
import { Note } from '../proto/notes_pb'
import { INotesServer } from '../proto/notes_grpc_pb'

const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

class NotesServer implements INotesServer {

}
```

**Tip**: if you're using [VSCode](https://code.visualstudio.com/?WT.mc_id=opensource-0000-ludossan), when you create a new class that implements an interface, a little blue lightbulb will show up next to it. Select it and there will be an option to implement all the members automatically:

![Blue lightbulb showing the automatic interface implementation option in VSCode](./image-5.png)

Once you click it, you'll have the interface members implemented with the required types. Just swap in the real implementation:

![Pre-implemented class with generic methods](./image-6.png)

Notice that, besides our methods, we have a TypeScript index signature at the top. This is one of the problems I mentioned when I said the libraries aren't originally built to work well with TypeScript. This bug is [documented](https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/v5.1.1/doc/server_impl_signature.md) in the library and, unfortunately, is a limitation of TypeScript for unknown index types: since the type says we can have `N` strings as keys and their values, we need to make it explicit that this class implements and allows this kind of thing too.

> When using only the `grpc` lib, this doesn't happen because the types don't require a dynamic index signature like this

Let's implement our functions, starting with the `list` function, which is the simplest. Let's add a signature the way the type demands:

```typescript
import { sendUnaryData, ServerUnaryCall, UntypedHandleCall } from '@grpc/grpc-js'
import { Note } from '../proto/notes_pb'
import { INotesServer } from '../proto/notes_grpc_pb'
import { Note, NoteListResponse, Void } from '../proto/notes_pb'

const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

class NotesServer implements INotesServer {

  list (_: ServerUnaryCall<Void, NoteListResponse>, callback: sendUnaryData<NoteListResponse>): void {
    const response = new NoteListResponse()
    notes.forEach((note) => {
      response.addNotes(
        (new Note).setId(note.id)
                  .setTitle(note.title)
                  .setDescription(note.description)
        )
    })
    callback(null, response)
  }

  [name: string]: UntypedHandleCall

}
```

Notice that now, instead of just returning the array, we're required to convert each item in the array into a `Note` and add it to the response type. While this is very good, because we get type safety, it's worse because we have to write more code, and there's no native way to convert the whole array at once from the compiler (you could, however, write a function that does exactly that).

## Talking about imports

Another important detail worth noticing: here we're receiving the `Void` type, which is a type we created ourselves inside our `.proto` file. If we want a more "correct" implementation, we can use the _well-known type_ called `Empty`, which ships with protobuf's standard library when you download it [from the official protoc repository](https://github.com/protocolbuffers/protobuf). You need to move this folder to `/usr/includes` or any other folder that's in your `PATH`.

Then we can import this type inside our `.proto` file:

```protobuf
syntax = "proto3";
package notes;

import "google/protobuf/empty.proto";

service Notes {
  rpc List (google.protobuf.Empty) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

// Entities
message Note {
  int32 id = 1;
  string title = 2;
  string description = 3;
}

message Void {}

// Requests
message NoteFindRequest {
  int32 id = 1;
}

// Responses
message NoteFindResponse {
  Note note = 1;
}

message NoteListResponse {
  repeated Note notes = 1;
}
```

We need to add a `package` declaration to say our file lives in a different namespace. This is mandatory so we don't confuse the compiler.

Then, in our server, we'd install the `google-protobuf` package with `npm i google-protobuf` and import the `Empty` type naturally:

```typescript
import { sendUnaryData, ServerUnaryCall, UntypedHandleCall } from '@grpc/grpc-js'
import { Note } from '../proto/notes_pb'
import { INotesServer } from '../proto/notes_grpc_pb'
import { Note, NoteListResponse } from '../proto/notes_pb'
import { Empty } from 'google-protobuf/empty_pb'

const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

class NotesServer implements INotesServer {

  list (_: ServerUnaryCall<Empty, NoteListResponse>, callback: sendUnaryData<NoteListResponse>): void {
    const response = new NoteListResponse()
    notes.forEach((note) => {
      response.addNotes(
        (new Note).setId(note.id)
                  .setTitle(note.title)
                  .setDescription(note.description)
      )
    })
    callback(null, response)
  }

  [name: string]: UntypedHandleCall

}
```

This was a small detour in the article to show that it's possible to keep things more separate and use external type libraries in our server. Even though **we won't be using this lib here**, it's an interesting bit to keep in mind.

## Back on track

Let's implement the `find` method. It follows the exact same idea: let's just add the basic type implementation and return a `Note` object:

```typescript
import { sendUnaryData, ServerUnaryCall, UntypedHandleCall } from '@grpc/grpc-js'
import { Note } from '../proto/notes_pb'
import { INotesServer } from '../proto/notes_grpc_pb'
import { Note, NoteFindRequest, NoteFindResponse, NoteListResponse, Void } from '../proto/notes_pb'

const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

class NotesServer implements INotesServer {

  list (_: ServerUnaryCall<Void, NoteListResponse>, callback: sendUnaryData<NoteListResponse>): void {
    const response = new NoteListResponse()
    notes.forEach((note) => {
      response.addNotes(
        (new Note).setId(note.id)
                  .setTitle(note.title)
                  .setDescription(note.description)
      )
    })
    callback(null, response)
  }

  find (call: ServerUnaryCall<NoteFindRequest, NoteFindResponse>, callback: sendUnaryData<NoteFindResponse>) {
    const id = call.request.getId()
    const foundNote = notes.find((note) => note.id === id)
    if (!foundNote) return callback(new Error('Note not found'), null)

    const response = new NoteFindResponse()
    response.setNote(
       (new Note()).setTitle(foundNote.title)
                   .setId(foundNote.id)
                   .setDescription(foundNote.description)
    )
    return callback(null, response)
  }

  [name: string]: UntypedHandleCall

}
```

Even though it's more verbose, the method becomes much easier to read and understand, on top of keeping cohesion and the required types.

To wrap up, let's start our server. We have two ways to do this: the first is via callback:

```typescript
import { INotesServer, NotesService } from '../proto/notes_grpc_pb'
import { Note, NoteFindRequest, NoteFindResponse, NoteListResponse, Void } from '../proto/notes_pb'
import { sendUnaryData, Server, ServerCredentials, ServerUnaryCall, UntypedHandleCall } from '@grpc/grpc-js'

const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

class NotesServer implements INotesServer {
  /* Our implementation */
}

const server = new Server()
server.addService(NotesService, new NotesServer())

server.bindAsync('0.0.0.0:50052', ServerCredentials.createInsecure(), (err, port) => {
  if (err) throw err
  console.log(`listening on ${port}`)
  server.start()
})
```

This is a problem coming from TypeScript, since it's possible to use `bindAsync` as a Promise in JavaScript. This happens because the server's type doesn't have a definition for a promise. But there's always a way around it, using `promisify`:

```typescript
import { promisify } from 'util'
import { INotesServer, NotesService } from '../proto/notes_grpc_pb'
import { Note, NoteFindRequest, NoteFindResponse, NoteListResponse, Void } from '../proto/notes_pb'
import { sendUnaryData, Server, ServerCredentials, ServerUnaryCall, UntypedHandleCall } from '@grpc/grpc-js'

const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

class NotesServer implements INotesServer {
  /* Our implementation */
}

const server = new Server()
server.addService(NotesService, new NotesServer())

const bindPromise = promisify(server.bindAsync).bind(server)

bindPromise('0.0.0.0:50052', ServerCredentials.createInsecure())
  .then((port) => {
    console.log(`listening on ${port}`)
    server.start()
  })
  .catch(console.error)
```

Much better, right? Note that we need to use `.bind(server)` because the `start()` method checks whether the server has already started by calling `this.started`.

Now, if we run `npm run start:server` we'll have our server running on port `50052`. All that's left is building the client, but notice how much better the workflow and understanding of the code got compared to before.

## Building the client

Building the client is almost entirely automatic, because we already have our `genericClientConstructor` called with the server type we need in `NotesService`. Create a new file in `src` called `client.ts`, and it barely needs any explaining:

```typescript
import { ChannelCredentials } from '@grpc/grpc-js'
import { NotesClient } from '../proto/notes_grpc_pb'
import { NoteFindRequest, Void } from '../proto/notes_pb'

const client = new NotesClient('0.0.0.0:50052', ChannelCredentials.createInsecure())
client.list(new Void(), (err, notes) => {
  if (err) return console.log(err)
  console.log(notes.toObject())
})

client.find((new NoteFindRequest).setId(1), (err, note) => {
  if (err) return console.log(err)
  console.log(note.toObject())
})

client.find((new NoteFindRequest).setId(3), (err, note) => {
  if (err) return console.log(err.message)
  console.log(note.toObject())
})
```

The client is basically already instantiated with all its methods, but remember that every return type and every type we send **needs** to be a valid gRPC type in TypeScript's eyes, which is why we're creating a new empty class with `new Void()`. It looks a bit counterproductive, but it helps a lot in keeping everything we do cohesive.

Run `npm run client` and watch the magic happen!

## Conclusion

As we've seen, it's quite easy to turn everything into TypeScript, but we first need to understand what we want and understand the limitations of the tools, which can turn out to be a big problem since they don't really talk to each other much.

Even though gRPC with TypeScript is amazing, we still have a few steps to go to turn this setup into something more useful so everyone can use it. I'm trying to do that with a few libs like [protots](https://github.com/khaosdoctor/protots), to generate interfaces and not just types, so we can abstract a bit more of the functionality in a way that doesn't require a full gRPC implementation to get working types.

In the next parts of the series, we're going to cover a lot of things, like `buf` and `streams`, so like and share this post with your friends so we can spread the word about gRPC and show that it's not that hard to build an API that everyone can use in a unified way!
