Introduction
JavaScript, when used correctly, enables developers to write clean, modularised code. That said, the language contains some quirks that can prove confusing to more novice JavaScripters, such as function invocation patterns; developers will eventually become accustomed to its workings, but will encounter frustration along the way.
Enter TypeScript. Developed by Anders Hejlsberg, the lead architect of C#, it is a superset of JavaScript that introduces features such as static typing and generics, as well as ECMAScript 6 and 7 proposals such as lambda expressions. The compiler naturally provides type checking, which augments developers' confidence in their code.
The Project
In this tutorial, we are going to write a very simple chat server using Node.js and WebSockets, which enable real-time duplex communication via TCP. When a client sends a message to the server, it will be validated and broadcast to all the connected clients:
You can view the source code of what we will accomplish over at GitHub.
Although you can use Visual Studio to develop TypeScript projects, I have written this tutorial in a platform-independent manner, so you can build and run the server across Windows, Linux, and OS X.
Setting Up
If you haven't already, install Node.js. This will also install npm, the package manager that we will use to install our server's dependencies, as well as the TypeScript compiler. When ready, install said compiler by running npm install -g typescript
at the command line (note that on Windows, there is a configured Node.js Command Prompt that you can use.) You may need to be root or an administrative user to install Node modules globally.
Next, create a directory for the project. From the command line, enter it and run npm init
:
cd <your_project_directory>
npm init
This will commence a prompt that will ultimately create a package.json file which can contain dependency information and other various metadata.
The only dependency in our project is the ws package. Install it by running npm install --save ws
. This will add it to the package.json file, so that dependencies can subsequently be resolved with a simple invocation of npm install
.
Note: To build one of the dependencies required by ws, you will need to install Python 2. Python is not a production dependency of our project.
Before writing any code, it is a good idea to flesh out our project's directory structure. Within the root of your project, create the following folders:
- build - contains our compiled JavaScript
- declarations - contains any required TypeScript declaration files (more on these momentarily)
- src - contains our TypeScript source code
Declaration Files
"Can we code now?"
Not yet.
When using a third-party framework or library, the TypeScript compiler needs to understand how their public
APIs are structured; otherwise, our build will fail. TypeScript provides a mechanism via declaration files.
Writing these files can prove to be a monotonous task, and in my opinion is one of the shortcomings of the language. Fortunately, the brilliant DefinitelyTyped project hosts declaration files for a plethora of popular JavaScript technologies.
In our case, we need two declaration files; one for Node.js and ws respectively. They can be found here and here. Save these into the declarations folder.
We're almost ready to code, but just a quick caveat; ws's declaration file has a dependency on Node's declaration, but is pointing to the incorrect path. Open the ws.d.ts file and locate the following line:
/// <reference path="../node/node.d.ts" />
Replace it with:
/// <reference path="node.d.ts" />
These reference comments are used by the compiler to verify third party code against the expected public contract.
Now We Can Code!
Before writing our server code, let's write our data model. Create a file in the src directory called models.ts and write the following interface:
'use strict';
interface Message {
name: string;
message: string;
}
In case you're wondering about the seemingly arbitrary 'use strict';
string literal, read this.
Now write the following class, in the same file, that implements this interface:
export class UserMessage implements Message {
private data: { name: string; message: string };
constructor(payload: string) {
var data = JSON.parse(payload);
if (!data.name || !data.message) {
throw new Error('Invalid message payload received: ' + payload);
}
this.data = data;
}
get name(): string {
return this.data.name;
}
get message(): string {
return this.data.message;
}
}
A few things that may stand out to you in this code:
- the
export
keyword - publicly exposes an object to other TypeScript modules. The underlying mechanism behind this depends upon the underlying technology being used. In the context of Node.js, it attaches an appropriate property to its global exports
object private data: { [...] }
- this is utilising TypeScript's type checking mechanism to determine that the data field is an object containing two properties; name
and message
. Although some manual validation occurs in the constructor due to the dynamic nature of WebSocket data, this still provides some clarity with regards to the expected contract get name(): string { ...
- just like in C#, we can write accessors to achieve encapsulation of our data!
Now onto the exciting stuff; writing our WebSocket
server! In the src folder, create a file called server.ts, and write the following code:
'use strict';
import WebSocket = require('ws');
import models = require('./models');
var port: number = process.env.PORT || 3000;
var WebSocketServer = WebSocket.Server;
var server = new WebSocketServer({ port: port });
server.on('connection', ws => {
ws.on('message', message => {
try {
var userMessage: models.UserMessage = new models.UserMessage(message);
broadcast(JSON.stringify(userMessage));
} catch (e) {
console.error(e.message);
}
});
});
function broadcast(data: string): void {
server.clients.forEach(client => {
client.send(data);
});
};
console.log('Server is running on port', port);
Some points of interest regarding this code:
- the
import
keyword - we could use a variable declaration as one would in a vanilla Node.js server, but the beauty of this keyword is that the compiler will automatically build the relevant dependencies (sans Node modules). In our case, compiling server.ts will also compile models.ts. var port: number
- another demonstration of TypeScript's static typing - lambda expressions! Interestingly enough, these have been proposed in ECMAScript 6
Now we can compile our code. At the command line, ensure that you are in the root directory of the project and run the compiler:
tsc --removeComments --module commonjs --target ES5 --outDir build src/server.ts
Let's deconstruct tsc's arguments:
--removeComments
- there's really no need for comments in compiled production code --module commonjs
- this enables dependencies to be loaded through Node's CommonJS module system. Without this, the compiler can't build our models script --target ES5
- compiles to ECMAScript 5-compliant JavaScript. In our case, we need it to use property accessors, which are implemented in JavaScript using Object.defineProperty
; this was standardised in ECMAScript 5.1 --outDir build
- specifies our build directory src/server.ts
- the script we want to compile. Remember that this will also compile our models script
When the code has compiled, you can run it using node build/server
.
Try It Out!
In a real-life scenario, a frontend team would write a client app to consume the connection. To keep things simple, we can demonstrate that our server works by creating WebSocket
instances in the browser. They are supported in Chrome, Firefox, and IE10+.
Open your browser's developer console and write the following to create multiple WebSocket
connections:
var socket = new WebSocket('ws://localhost:3000');
socket.onmessage = function (message) {
console.log('Connection 1', message.data);
};
var socket2 = new WebSocket('ws://localhost:3000');
socket2.onmessage = function (message) {
console.log('Connection 2', message.data);
};
var socket3 = new WebSocket('ws://localhost:3000');
socket3.onmessage = function (message) {
console.log('Connection 3', message.data);
};
To send a message, invoke socket.send(JSON.stringify({ name: 'Bob', message: 'Hello' }));
. You should see that all three connections receive the data:
My Thoughts on TypeScript
As a JavaScript developer, I'm familiar enough with most of the quirks and idiosyncrasies of the language, so there isn't a fundamental motivation to use TypeScript in all of my projects. As a C# developer, however, I like the features that TypeScript brings to the table; static typing, interfaces, generics, lambdas, and all its other goodness. Personally, I would feel obliged to use it in a browser or Node.js project that required granular validation of data.
I hope that you found this tutorial useful. Please feel free to get in touch if you have any questions!
James is a full-stack software developer with a passion for web technologies. He is currently working with a variety of languages, and has engineered solutions for the likes of Sky, Channel 4, Trainline, and NET-A-PORTER.