A library for fully typed client-server communication with socket.io and TypeScript.
Branch: master
Clone or download
Latest commit fe1434d Feb 6, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
example restructuring Feb 26, 2018
.gitignore readme and example Jan 20, 2018
.npmignore v0.2.3 Jan 20, 2018
.prettierrc fix formatting Jan 20, 2018
README.md rename examples Feb 26, 2018
TypedClient.ts socketio opts Apr 3, 2018
TypedServer.ts upgrade dependencies Feb 5, 2019
package.json v0.5.8 Feb 5, 2019
tsconfig.json upgrade dependencies Feb 5, 2019
typedSocket.d.ts v0.5.5 Jun 2, 2018
util.ts allow mapping errors Feb 5, 2018
yarn.lock upgrade dependencies Feb 5, 2019

README.md

typed-socket.io

A library for fully typed client-server communication with socket.io and TypeScript.

Install via npm: typed-socket.io

Source code on GitHub: phiresky/typed-socket.io

Basic Usage (purely compile-time)

Simple Example:

Let's say you want to have a simple chat server, where users can connect and send messages to an english and a russian channel, and all received messages will be forwarded to all other connected users.

Using this library, you can do this with both the server and the client having 100% type safety. Here's how it looks:

  • server.ts
import * as io from "socket.io";

const server = io(8000) as TypedServer;
const chatServer = server.of("/chat");

chatServer.on("connection", client => {
    client.on("postMessage", (info, callback) => {
        // typeof info.message === string
        // typeof info.channel === "en" | "ru"
        chatServer.emit("chatMessage", {
            ...info,
            sender: client.id,
            /* something: 1,

          ┏━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
          ┃ [ts] Object literal may only specify known properties,        ┃
          ┃      and 'something' does not exist in type                   ┃
          ┃ '{ sender: string; message: string; channel: "en" | "ru"; }'. ┃
          ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ */
        });
        callback(null, "ok");
    });

    /* client.emit("otherMessage", 123);

              ┏━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
              ┃ [ts] Argument of type '"otherMessage"' is not assignable  ┃
              ┃      to parameter of type '"chatMessage"'.                ┃
              ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ */
});
  • client.ts
import * as io from "socket.io-client";

const host = "http://localhost:8000";
const ns = "/chat";
const client: TypedChatClient = io(host + ns) as any;

const channelNames = { en: "English", ru: "Russian" };

client.on("chatMessage", ({ sender, message, channel }) => {
    // assert typeof channel === "en" | "ru"
    // assert typeof message === string
    console.log(`${channelNames[channel]}: ${sender}: ${message}`);
});

client.emit(
    "postMessage",
    { message: "Hello World", channel: "en" },
    (error, response) => {
        if (error) return console.error(error);
        // typeof response === "ok" here
    },
);

client.emit("postMessage", { message: "Hello World", channel: "es" }, () => {});
/*
              ┏━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
              ┃ [ts] Type '"es"' is not assignable to type '"en" | "ru"'. ┃
              ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ */

The only thing you need to glue this together is a file that is imported on the client and the server with the type definition, that looks as follows:

  • common.ts
import { ServerDefinition, SimpleNamespace,
         RootServer, ClientSideSocket } from "typed-socket.io";

type ChatMessage = {
    sender: string;
    message: string;
    channel: "en" | "ru";
};
export interface MyServerDefinition extends ServerDefinition {
    namespaces: {
        "/chat": SimpleNamespace<{
            // messages the server may send to the clients
            ServerMessages: {
                chatMessage: ChatMessage;
                history: ChatMessage[];
            };
            // messages clients can send to the server, with a typed response
            ClientRPCs: {
                postMessage: {
                    request: { message: string; channel: "en" | "ru" };
                    response: "ok";
                    error: string;
                };
            };
            // messages clients can send to the server (without a response)
            ClientMessages: {
                // (not needed here)
            };
        }>;
        // ...
    };
}

export type TypedServer = RootServer<MyServerDefinition>;
export type TypedChatClient = ClientSideSocket<MyServerDefinition, "/chat">;

A runnable version of this example is in example/basic-chat.

Note that when using this library this way, the types are only checked at compile-time, which means that you trust your clients to send the correct data at runtime.

Promises

For all RPCs, you can also use promises via .onAsync instead of .on on the server and and .emitAsync instead of .emit on the client, by calling promisifySocket(client) from typed-socket.io/util.

// server
chatServer.on("connection", client => {
    client.on("postMessage", async info => {
        chatServer.emit("chatMessage", {
            ...info,
            sender: client.id,
        });
        return "ok"; // must return "ok" here (or throw), because thats what the ServerDefinition says.
    });
});

// client
const response = await client.emitAsync(
    "postMessage",
    { message: "Hello World", channel: "en" }
);
// assert response === "ok"

Runtime Component

If you want runtime type safety, you can use the following classes:

TypedServer

By using this class all client requests will be type checked and rejected if they don't match the given schema.

Example: See example/chat-with-runtime-checks/server.ts

Needs a peer dependency to socket.io.

TypedClient

This class ensures that you listen to all messages a server can send.

Example: See example/chat-with-runtime-checks/client.ts

Needs a peer dependency to socket.io-client.