The complete gRPC guide part 3: types everywhere with TypeScript!
- The complete gRPC guide part 1: What is gRPC?
- The Complete gRPC Guide Part 2: Hands-on with JavaScript
- The complete gRPC guide part 3: types everywhere with TypeScript! (you are here)
- The Complete gRPC Guide Part 4: Streams
Part 3 of 4 of the series The complete gRPC guide
In our previous article of the guide 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 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, 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…
That said, the tooling we have for JS 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 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
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, 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:
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:
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 with decorators.
What we want#
Let’s think about this a bit. We have a complete definition file, like this one:
syntax = "proto3";
service Notes { rpc List (Void) returns (NoteListResponse); rpc Find (NoteFindRequest) returns (NoteFindResponse);}
// Entitiesmessage Note { int32 id = 1; string title = 2; string description = 3;}
message Void {}
// Requestsmessage NoteFindRequest { int32 id = 1;}
// Responsesmessage 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, since the lib is written in TypeScript. See more of the types in the official repository.
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:
abstract class Server { function addService (service: ServiceDefinition, implementation: UntypedServiceImplementation): void}And the ServiceDefinition and UntypedServiceImplementation types have, respectively, the following definitions:
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”:
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:
export type MethodImplementation = (call: ServerUnaryCall<RequestInput, RequestOutput>, callback: sendUnaryData<RequestOutput>): voidWe could even make it prettier by doing everything with generics:
export type MethodImplementation<In, Out> = (call: ServerUnaryCall<In, Out>, callback: sendUnaryData<Out>): voidAnd 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:
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 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
syntax = "proto3";
service Notes { rpc List (Void) returns (NoteListResponse); rpc Find (NoteFindRequest) returns (NoteFindResponse);}
// Entitiesmessage Note { int32 id = 1; string title = 2; string description = 3;}
message Void {}
// Requestsmessage NoteFindRequest { int32 id = 1;}
// Responsesmessage 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:
$ npm i -D @types/long @types/node grpc_tools_node_protoc_ts grpc-tools typescriptAs you can see, we’re going to use the 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, 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:
{ "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:
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/*.protoThis 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_jskey in the second part of the command. That’s because this library is prepared to generate types both forgrpcand 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:
{ // ... 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:
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 messagesWhere 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:
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:
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:
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:
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, 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:

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

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 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
grpclib, 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:
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. 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:
syntax = "proto3";package notes;
import "google/protobuf/empty.proto";
service Notes { rpc List (google.protobuf.Empty) returns (NoteListResponse); rpc Find (NoteFindRequest) returns (NoteFindResponse);}
// Entitiesmessage Note { int32 id = 1; string title = 2; string description = 3;}
message Void {}
// Requestsmessage NoteFindRequest { int32 id = 1;}
// Responsesmessage 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:
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:
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:
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:
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:
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, 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!