Skip to main content
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Francine Brady
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Francine Brady ( @darkfeather )

Experimenting With ts-node And Using TypeScript In Node.js On The Server

By on

Some months ago, I was talking to Ward Bell about traspiling TypeScript on the server so that I could use it in Node.js. I've had to learn TypeScript as part of my Angular 2 studies; and, while it presented some friction at first, it has gone on to become one of the features that I like most about the Angular 2 platform. So, naturally, I wanted to see if I could use it in other JavaScript contexts. To this point, however, Ward explained that I don't actually need to transpile TypeScript in order to use it in Node.js - instead, I can just use "ts-node" and run TypeScript right on the server. Well, months later, I've decided it's time to sit down and actually play around with ts-node.

To be clear, when running TypeScript in Node.js, the TypeScript source code is still being transpiled - Node.js doesn't magically support TypeScript syntax. But, by running your Node.js application though the ts-node executable (instead of the node executable), a TypeScript wrapper is registered with the "require" module to load the .ts file-extension. This allows the TypeScript compiler to act as the intermediary between your TypeScript source code and the Node.js runtime. As such, there is transpilation; but you - as the developer - don't have to know anything about it.

Of course, the idea of transpiling your code in a production environment may seem scary; but, it's not nearly as scary as it sounds:

  • Unrelated to ts-node, Node.js caches modules in memory once they've been resolved. This means that, for the life-cycle of the application, you only incur the cost of transpiling TypeScript once per file. All subsequent requests to the same module will be pulled right of memory.

  • Since all of the require() statements [should] appear at the top of your Node.js files, the entire dependency graph can be calculated at boot time. This means that all TypeScript files should be loaded and transpiled before you have a chance to bind your HTTP server to a port. This means that the cost of transpilation is never felt by the user since your HTTP server will never start accepting traffic until after the TypeScript files have been loaded into memory.

  • ts-node can run with a "-F" or "--fast" option that executes the underlying TypeScript compiler in transpileModule mode. This tranforms the TypeScript code but skips all of the low-level type-checking and validation. By using this flag in production, you can keep the compile time very low while still getting all of the type-checking magic in your local development environment.

  • ts-node caches the transpiled output on disk. This means that if the underlying process crashes and is restarted (by something like Forever or Pm2), there is essentially zero overhead to the TypeScript transpilation since the pre-compiled files are already accessible.

All to say, ts-node does have some overhead. But, that overhead is actually quite small and will never be felt by your users. In fact, in the ts-node Issues, Blake Embrey - ts-node project creator/maintainer - even says that using it in production should have no significant overhead.

To experiment with using ts-node, I wanted to try and build a simple Express.js application using TypeScript. Luckily, Express.js - and many other common Node.js modules - have Type definition files in the Definitely Typed project. This makes it easy to bring non-TypeScript modules into a TypeScript project. You can see these Definitely Typed modules as "@types" dependencies in my package.json file:

{
	"name": "tsnode",
	"version": "1.0.0",
	"description": "Experimenting with ts-node.",
	"scripts": {
		"dev": "nodemon --exec 'ts-node --cache-directory .tscache' ./server.ts",
		"start": "ts-node --fast ./server.ts"
	},
	"author": "Ben Nadel",
	"license": "ISC",
	"dependencies": {
		"@types/body-parser": "^1.16.3",
		"@types/chalk": "^0.4.31",
		"@types/express": "^4.0.35",
		"@types/node": "^7.0.18",
		"body-parser": "^1.17.1",
		"chalk": "^1.1.3",
		"express": "^4.15.2",
		"nodemon": "^1.11.0",
		"ts-node": "^3.0.4",
		"typescript": "^2.3.2"
	}
}

As you can see, I am pulling in Definitely Typed definitions for Express, chalk, and node.

You can also see that I have two different run-scripts for this project. The one that I would use in my local development environment:

nodemon --exec 'ts-node --cache-directory .tscache' ./server.ts

... which runs through Nodemon so that I can restart the process whenever files change. And, the one that I may use in production (although, I haven't actually tried this yet):

ts-node --fast ./server.ts

Notice that the run-script I use for production included the "--fast" flag so that it executes the transpilation but skips all of the low-level type-checking and validation. This should keep the boot-time for my production app nice and low.

NOTE: In the development environment, I am overriding the cache-directory just so I can see how ts-node transpiles and caches the files. There's no need to actually do this unless you want to look at the contents.

By default, ts-node will look for your tsconfig.json file in the same directory. You can override this location with the "-P" and "--project" flags. I just kept might right in the root directory:

{
	"compilerOptions": {
		"emitDecoratorMetadata": true,
		"experimentalDecorators": true,
		"lib": [
			"ES6"
		],
		"module": "commonjs",
		"moduleResolution": "node",
		"noImplicitAny": true,
		"noUnusedLocals": true,
		"pretty": true,
		"removeComments": false,
		"sourceMap": false,
		"suppressImplicitAnyIndexErrors": true,
		"target": "es6"
	}
}

With my dependencies installed and my TypeScript compiler configured, I then went about creating a strongly-typed Express.js application. For this demo, I am creating a simple website that presents two end-points relating to Greetings:

  • GET /greet/:name - Presents a personal greeting to the given name.
  • POST /greeting - Allows the greeting template to be updated for future "/greet" usage.

In the following router configuration, I have two error handlers. I am doing this just because I was having fun with chained exceptions - don't try to see any practical usage beyond some fun and games:

// Require the core node modules.
import { Application } from "express";
import bodyParser = require( "body-parser" );
import chalk = require( "chalk" );
import express = require( "express" );
import { NextFunction } from "express";
import { Request } from "express";
import { Response } from "express";

// Require the application modules.
import { AppError } from "./lib/app-error";
import { ErrorLogger } from "./lib/error-logger";
import { Greeter } from "./lib/greeter";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// Initialize our application models.
var errorLogger = new ErrorLogger();
var greeter = new Greeter( "Hello %s, I hope you are well!" );

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// Initialize the Express application.
var app: Application = express();
app.use( bodyParser.urlencoded({ extended: false }) );
app.use( bodyParser.json() );

// ROUTE: I return a greeting for the provided user.
app.get(
	"/greet/:name",
	function( request: Request, response: Response, next: NextFunction ) : void {

		response
			.type( "text/plain" )
			.send( greeter.greet( request.params.name ) )
		;

	}
);

// ROUTE: I update the greeting template being used by the greeter.
app.post(
	"/greeting",
	function( request: Request, response: Response, next: NextFunction ) : void {

		greeter.setGreetingTemplate( request.body.template );
		response
			.type( "text/plain" )
			.send( greeter.greet( "{NAME GOES HERE}" ) )
		;

	}
);

// ERROR HANDLING: I handle any error emitted by the applied routes.
app.use(
	function( error: any, request: Request, response: Response, next: NextFunction ) : void {

		// CAUTION: Just wrapping an error so that the error will be CHAINED by the
		// time it gets to the errorLogger (so I can experiment with logging a chained
		// error). There is no practical point to having multiple error handlers - well
		// not like this, any way.
		try {

			throw(
				new AppError({
					message: "Thrown in Try Block",
					rootCause: error
				})
			);

		} catch ( innerError ) {

			throw(
				new AppError({
					message: "Thrown in Catch Block",
					rootCause: innerError
				})
			);

		}

	}
)

// ERROR HANDLING: I handle any error emitted by the applied routes.
app.use(
	function( error: any, request: Request, response: Response, next: NextFunction ) : void {

		errorLogger.log( error );
		response
			.status( 500 )
			.type( "text/plain" )
			.send( "Something went wrong!" )
		;

	}
)

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// Start listening on the public port.
app.listen(
	3000,
	function() : void {

		console.log( chalk.bold.green( "Application listening on port :3000." ) );

	}
);

// Listen for uncaught exceptions - these are errors that are thrown outside the
// context of the Express.js route handlers and other proper async request handling.
process.on(
	"uncaughtException",
	function handleError( error: any ) : void {

		// NOTE: We know that the error-logger is available at this point because the
		// process event-handler would have never been attached if we didn't make it
		// this far in the control flow of the page.
		errorLogger.log( error );

	}
);

As you can see, this Express.js app looks pretty much like any other Express.js app, only it's using TypeScript, which means that we can use type annotations and other special syntax. For Express.js, I am actually importing interfaces from the Definitely Typed Express library:

  • import { Application } from "express";
  • import { NextFunction } from "express";
  • import { Request } from "express";
  • import { Response } from "express";

... which I can then use to annotate my main app and its route handler callbacks. The annotations make the code a bit more verbose; but, that verbosity brings compile-time type safety. And, it makes the code self-documenting. For example, you can see that the "error" argument is of type "any", not of type, "Error". This is because you can throw any type of value in JavaScript; and, Express' next() method can take "any" argument when invoked. As such, the error parameter in the error handlers can make no guarantee about what value (or properties on that value) will be present.

Now, if you look at the list of imports at the head of the file, you'll see that I am not explicitly listing file extensions. This is because the ".ts" file extension was registered by the ts-node wrapper and will be used to find and resolve TypeScript modules. When the server.ts file loads, Node.js will start to traverse the dependency graph, represented by the require() calls, which will, in turn, load and compile other TypeScript modules.

In my case, that main TypeScript module that I'm using is Greeter.ts. This module powers all of the end-points in the Express.js application:

// Require the core node modules.
import util = require( "util" );

// Require the application modules.
import { AppError } from "./app-error";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

export class Greeter {

	private greetingTemplate: string;


	// I initialize the greeter.
	constructor( greetingTemplate: string ) {

		this.setGreetingTemplate( greetingTemplate );

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// I generate a greeting for the given name.
	public greet( name: string ) : string {

		return( util.format( this.greetingTemplate, this.normalizeName( name ) ) );

	}


	// I update the greeting template used to generate greetings.
	public setGreetingTemplate( greetingTemplate: string ) : void {

		this.testGreetingTemplate( greetingTemplate );
		this.greetingTemplate = greetingTemplate;

	}


	// ---
	// PRIVATE METHODS.
	// ---


	// I normalize the format of the given name for use in the template. This value was,
	// presumably, plucked out of a URL and may not be formatted naturally.
	private normalizeName( name: string ) : string {

		var normalized = name
			.toLowerCase()
			// Replace the leading-word-boundary letters with upper-case versions.
			.replace(
				/\b[a-z]/g,
				( $0: string ) : string => {

					return( $0.toUpperCase() );

				}
			)
		;

		return( normalized );

	}


	// I test the greeting template to ensure that setting it won't create an invalid
	// state for the class. If the template is valid, I exit quietly; if the template
	// is invalid, I throw an error.
	private testGreetingTemplate( greetingTemplate: string ) : void {

		if ( ! greetingTemplate ) {

			throw(
				new AppError({
					message: "Greeting template cannot be empty."
				})
			);

		}

		if ( ! greetingTemplate.includes( "%s" ) ) {

			throw(
				new AppError({
					message: "Greeting template must contain one [%s] placeholder.",
					extendedInfo: {
						template: greetingTemplate
					}
				})
			);

		}

	}

}

With this TypeScript module, we can now start to play with the Express.js application using Postman. First, I can make a request to get a greeting for a given name:

Using ts-node to leverage TypeScript in Node.js with Express.js.

As you can see, if we make a request to:

http://localhost:3000/greet/simon%20phoenix

... we get back the greeting from our Greet.ts module:

Hello Simon Phoenix, I hope you are well!

Now, let's try to POST a new greeting template:

Using ts-node to leverage TypeScript in Node.js with Express.js.

With the new greeting template in place, we can name make another GET request back to the end-point:

http://localhost:3000/greet/simon%20phoenix

... and we get the new greeting:

Mellow greetings, Simon Phoenix. What seems to be your boggle?

This demo is rather trivial; but, it's very exciting to see how easy it is to start using TypeScript with Node.js and Express.js.

For completeness, here are the other two TypeScript modules I created for the demo. First is the AppError.ts, which is my custom Error sub-class:

interface AppErrorOptions {
	message: string;
	rootCause?: any;
	extendedInfo?: ExtendedInfo;
}

interface ExtendedInfo {
	[key: string]: any;
}

// CAUTION: We can only extend the native Error object because of the VERSION OF NODE
// we are using (which supports ES6) - this is NOT a feature of TypeScript.
export class AppError extends Error {

	public extendedInfo: ExtendedInfo;
	public rootCause: any;


	// I initialize the custom application error.
	constructor( options: AppErrorOptions ) {

		super( options.message );
		this.name = "AppError";
		this.rootCause = ( options.rootCause || null );
		this.extendedInfo = ( options.extendedInfo || null );

		// Capture the current stack trace and store it in the property "this.stack".
		Error.captureStackTrace( this, this.constructor );

	}

}

This module really shows off one of the things I love most about TypeScript - its ability to document hash-based arguments. Here, instead of passing in individual arguments to the AppError() constructor, I'm passing in a single hash. And, in this case, it's easy to see exactly what is and is not allowed in that hash by looking at the AppErrorOptions Interface. Bro! That's like hella self-documenting.

And then, the ErrorLogger() which logs my errors to the console. The ErrorLogger() is more complicated than it needs to be because I was having so much fun playing with TypeScript. For example, I had to use the imported Interface - ChalkChain - from the Chalk library in order to be able to pass Chalk style methods around as naked Function objects.

// Require the core node modules.
import chalk = require( "chalk" );
import { ChalkChain } from "chalk";
import util = require( "util" );

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// I pretty-print errors to the console / terminal.
export class ErrorLogger {

	// ---
	// PUBLIC METHODS.
	// ---


	// I log the given error to the terminal
	public log( error: any ) : void {

		console.log( chalk.red.bold( "An Error Occurred" ) );
		console.log( chalk.red.bold( "=================" ) );
		this.printError( error );

	}


	// ---
	// PRIVATE METHODS.
	// ---


	// I recursively print the given error to the console.
	private printError( error: any, depth: number = 0 ) : void {

		var prefix = depth
			? ( "-".repeat( ( depth * 4 ) + ( 2 * ( depth - 1 ) ) ) + "|" )
			: ""
		;

		// If the given error is not an instance of the Error class, then we can't really
		// depend on it having any particular shape. If so, just log to the console and
		// terminate the algorithm.
		// --
		// CAUTION: We're explicitly not using a TYPE GUARD here because the negation of
		// the type guard appears to confuse the TypeScript compiler.
		if ( ! error || ! error.message ) {

			this.printLines( prefix, this.serialize( error ), chalk.black );
			return;

		}

		this.printLines( prefix, error.message, chalk.red );

		// NOTE: We can check for this, even though it's not a known property on an Error
		// object because we are defining the parameter as ":any", which tells Typescript
		// to skip type validation in this method (for this value).
		if ( error.extendedInfo ) {

			this.printLines( prefix, this.serialize( error.extendedInfo ), chalk.black );

		}

		if ( error.stack ) {

			this.printLines( prefix, error.stack, chalk.dim );

		}

		// NOTE: We can check for this, even though it's not a known property on an Error
		// object because we are defining the parameter as ":any", which tells Typescript
		// to skip type validation in this method (for this value).
		// --
		// NOTE: The depth-condition here is to prevent stack exhaustion on the chance
		// that the nested error structure contains a circular reference.
		if ( error.rootCause && ( depth < 5 ) ) {

			this.printError( error.rootCause, ( depth + 1 ) );

		}

	}


	// I print the given potentially-multi-line value to the console, prepending the
	// given prefix to each line.
	private printLines( prefix: string, value: string, colorize: ChalkChain ) : void {

		value
			.split( /[\r\n]+/g )
			.slice( 0, 5 ) // Truncate the number of lines we output (for demo).
			.forEach(
				( line: string ) : void => {

					prefix
						? console.log( chalk.dim( prefix ), colorize( line ) )
						: console.log( colorize( line ) )
					;

				}
			)
		;

	}


	// I safely serialize the given value for logging.
	private serialize( value: any ) : string {

		// Ideally, we want to use JSON because it allows us to "pretty print" the value
		// to a string. However, JSON can't handle circular references; so, if it fails,
		// we'll fall-back to the safer Util module, which can handle circular
		// references, but doesn't let us format the value as easily.
		try {

			return( JSON.stringify( value, null, 4 ) );

		} catch ( error ) {

			return( util.inspect( value ) );

		}

	}

}

If you haven't used TypeScript, it may seem like it adds unnecessary constraints. But it's the constraints that you end up loving. By using type annotations, it self-documents the code and forces you to think about how your Objects are being passed around; and, about what assumptions can be made by a consuming context. The type-safety is definitely nice; but its not fool-proof and, at least for me, it's only a secondary benefit. It's nice to see how easily TypeScript can be used in a Node.js environment; and, with very little overhead - and certainly no overhead that your users will ever see.

Want to use code from this post? Check out the license.

Reader Comments

1 Comments

Thank you for the article. I get the error shown below when running :
nodemon --exec 'ts-node --cache-directory .tscache' ./server.ts

I think the issue is that my it did not create a 'lib' folder with the compiled js files. Did I miss a step that is required to generate the 'lib' folder?

C:\DevApps\nodejs\node_modules\ts-node\src\index.ts:300
throw new TSError(formatDiagnostics(diagnosticList, cwd, ts, lineOffset))
^
TSError: ? Unable to compile TypeScript
server.ts (11,26): Cannot find module './lib/app-error'. (2307)
server.ts (12,29): Cannot find module './lib/error-logger'. (2307)
server.ts (13,25): Cannot find module './lib/greeter'. (2307)
at getOutput (C:\DevApps\nodejs\node_modules\ts-node\src\index.ts:300:15)
at C:\DevApps\nodejs\node_modules\ts-node\src\index.ts:330:16
at Object.compile (C:\DevApps\nodejs\node_modules\ts-node\src\index.ts:489:17)
at Module.m._compile (C:\DevApps\nodejs\node_modules\ts-node\src\index.ts:382:43)
at Module._extensions..js (module.js:579:10)
at Object.require.extensions.(anonymous function) [as .ts] (C:\DevApps\nodejs\node_modules\ts-node\src\index.ts:385:12)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
at Function.Module._load (module.js:438:3)
at Function.Module.runMain (module.js:604:10)

15,688 Comments

@Scott,

Yeah, it looks like your `server.ts` file is trying to import several dependencies from the `./lib/` directory:

```
// Require the application modules.
import { AppError } from "./lib/app-error";
import { ErrorLogger } from "./lib/error-logger";
import { Greeter } from "./lib/greeter";
```

Those three things should be in a lib directory. Or, if they are in the top-level directory, just remove the `./lib` prefix from the path.

1 Comments

I'm getting error that "ts-node' is not recognized as an internal or external command, operable program or batch file" while working in your experiment "Experimenting With ts-node And Using TypeScript In Node.js On The Server"

15,688 Comments

@Mugesh,

It sounds like you are not pointing to the ts-node binary in the node_modules folder. If you use the "npm run" scripts to invoke the ts-node binary, it will automatically look in the node_modules folder. This is why I have:

"start": "ts-node --fast ./server.ts"

... in my package.json file. So I can run:

npm run start

... and it will look in the right place. If you don't use an npm run script, you just have to be more explicit with the ts-node location:

./node_modules/.bin/ts-node --fast ./server.ts

Hopefully that helps.

15,688 Comments

@Spandana,

As a sanity check, did you run "npm install" first? It's possible you don't have the dependencies installed yet.

1 Comments

I am having issue in debugging with ts-node. It is not generating source map. I am doing node --debug-brk ts-node server.ts . Debugger is attaching but it is saying breakpoint will not hit as source map is not generated when I am putting breapoint.

1 Comments

You can use import statements on modules without default exports:

```
import * as express from 'express'
```

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel