Now that we can check types, we can check inputs to function calls! RPC means “remote function calls” and works mostly the same way as local ones, you pass data in and (sometimes) get data out. So we’ll need to describe the “input” and “output” of our function calls. Let’s use a simple convention of i for input and o for output:

import * as t from 'io-ts'; const ConvertToNumber = {

i: t.string,

o: t.number,

};

Later we’re going to need to do two things with this type: implement a function handler for it (on the server), and call a function described by it (on the client).

It could look something like this:

handle(ConvertToNumber, "ConvertToNumber", (str) => parseInt(str)); call(ConvertToNumber, "ConvertToNumber", '123');

But that’s not very safe, we type the name twice! We’re going to need a name for each message, so how about we generate it automatically somehow? The only way I know how to do that is by turning it into a function. Then it will automatically have .name set for us.

const ConvertToNumber = () => ({

i: t.string,

o: t.number,

}); ConvertToNumber.name // 'ConvertToNumber'

Great, even less work for us!

In order to write “handler” and “caller” functions that have our i and o types, we need to “extract” them into I and O types from this type definition, so that TypeScript can infer it. So let’s create an interface that matches the shape we just made.

export interface FN<I, O> {

i: t.Type<I>,

o: t.Type<O>,

};

Thanks to t.Type , TypeScript is able to reach into our original definition and pull out I=string and O=number from our “ConvertToNumber” example.

Now your handler-producing and caller-wrapping functions just need to have these signatures:

function handle<I,O>(decl: FN<I,O>, fn: (input: I) => O) function call<I,O>(decl: FN<I,O>, args: I) => O

We would use these like so:

handle(ConvertToNumber, (str) => parseInt(str)); call(ConvertToNumber, '123') // 123

Looking good so far! But there’s a catch. TypeScript can’t easily infer the second parameter in either handle or call from the first parameter, so it can’t warn us if we pass the wrong type. I think this is because TypeScript can’t tell which is the “master” type and which is dependent on it, since the parameters are siblings. The only way I know how to get around this is to force the flow of type inference downward into a returned function:

function handle<I,O>(decl: FN<I,O>) =>

(fn: (input: I) => O) =>

void; function call<I,O>(decl: FN<I,O>) =>

(args: I) => O; handle(ConvertToNumber)((str) => parseInt(str)); call(ConvertToNumber)('123'); // 123

This is a bit ugly, but at least now we get proper warnings if we return anything other than a number, or if we try to use the input as anything other than a string. And the IDE shows us correctly that it is a string, and gives us all the right auto-completions!