v6

In this example, we will guide you through the steps to turn your project into a TypeScript project. After the guide have been completed, you will benefit from strong type checking and code completion.

Use this example only for versions under 6

Requirements

In this example, we will assume that you are familiar with TypeScript and that you have it installed. If this is not the case, simply run the following to get packages you will need in this Woodshop: npm install --save-dev typescript tsc-watch nodemon

To make it easy for you, we created our own typings to help you using our exposed tools using TypeScript. They are available as @types/forest-express-sequelize and @types/forest-express-mongoose. To get them, simply run: npm install --save-dev @types/forest-express-[sequelize | mongoose]

Configuration

As for every TypeScript project, we need to create a configuration file. It helps the transpiler to know where to take the files from, and where to transpile them to. To have a smooth migration into TypeScript, we will use the following configuration:

Add this file at the root of your project.

tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "pretty": true,
    "sourceMap": false,
    "target": "es2017",
    "outDir": "./dist",
    "baseUrl": "./",
    "types" : ["node", "express", "forest-express-sequelize", "sequelize"],
    "allowJs": true
  },
  "include": ["./**/*", ".env"],
  "exclude": ["node_modules", "dist"]
}

This tells the transpiler to take every file using .ts or .js (see allowJs option) as an extension, and to transpile them into a ./dist folder. This is where your transpiled files will be. This type of configuration allows you to translate your app into TypeScript in dribs and drabs. No need to translate every file at once, change only the files that interest you. Now that your TypeScript transpiler is set, we need to update our .package.json with new scripts to fit with the new structure of our project.

package.json
"scripts": {
  "start": "node ./dist/server.js",
  "legacy-start": "node server.js",
  "build": "tsc",
  "start-dev": "tsc-watch --project ./tsconfig.json --noClear --onSuccess \"nodemon --watch ./dist ./dist/server.js\""
},

Note the last script start-dev is the script to use while developing. It will transpile your sources every time you perform a change, and it will refresh your server. Finally, let's install the mandatory typings for a newly generated project, run this from a command prompt:

npm install --save-dev @types/node @types/express @types/sequelize @types/forest-express-sequelize

And that's it! You are now able to code using TypeScript in your app. Simply change the extension from .js to .ts , change some code, and your file will be automatically handled.

How it works

Directory: /models

Depending on your ORM (sequelize, mongoose) your models' configuration will change.

Sequelize Models

The idea here is to create a TypeScript class which will describe how your data should look like, and then init your actual model using sequelize. Let's take a simple user model, generated by Lumber:

models/users.js
module.exports = (sequelize, DataTypes) => {
  const { Sequelize } = sequelize;
  
  const Users = sequelize.define('users', {
    firstname: {
      type: DataTypes.STRING,
    },
    lastname: {
      type: DataTypes.STRING,
    },
    createdAt: {
      type: DataTypes.DATE,
    },
    updatedAt: {
      type: DataTypes.DATE,
    },
  }, {
    tableName: 'users',
    schema: process.env.DATABASE_SCHEMA,
  });

  return Users;
};

Let's create a TypeScript class to describe our data, based on this model. Create a folder alongside models to store interfaces, and create the users.ts interfaces:

interfaces/users.ts
import { Model } from "sequelize";

export default class Users extends Model {
    public id!: number;
    public firstname!: string;
    public lastname!: string;
    public readonly createdAt: Date;
    public readonly updatedAt: Date;
}

Now our data are typed, let's go back to our model models/users.js , switch the file to TypeScript, and change its content with the following:

models/users.ts
import Users from '../interfaces/users';

export default (sequelize, DataTypes) => {
  Users.init(
    {
      id: {
        type: DataTypes.INTEGER.UNSIGNED,
        autoIncrement: true,
        primaryKey: true
      },
      firstname: {
        type: new DataTypes.STRING(128),
        allowNull: false
      },
      lastname: {
        type: new DataTypes.STRING(128),
        allowNull: false
      }
    },{
        tableName: "users",
        modelName: 'users',
        sequelize,
    }
  );

  return Users;
}

Note the following:

  • The import statement has changed.

  • We force the modelName property to ensure our model sticks to its previous name.

And that's it! Your model is now defined in TypeScript, you can use it anywhere you want by importing it using:

import users from './interfaces/users';

You can find more resource here about how to use TypeScript with the sequelize ORM:

Directory: /routes

Routes consist of a simple router, handling CRUD routes, and using models/record tools to manipulate data.

So there is nothing special to do in this section, but changing the Javascript import statements to TypeScript ones. Here are the default routes created for the collection users:

routes/users.js
const express = require('express');
const { PermissionMiddlewareCreator } = require('forest-express-sequelize');
const { users } = require('../models');

const router = express.Router();
const permissionMiddlewareCreator = new PermissionMiddlewareCreator('users');

// Create a User
router.post('/users', permissionMiddlewareCreator.create(), (request, response, next) => {
  next();
});

// Update a User
router.put('/users/:recordId', permissionMiddlewareCreator.update(), (request, response, next) => {
  next();
});

...

// Delete a list of Users
router.delete('/users', permissionMiddlewareCreator.delete(), (request, response, next) => {
  next();
});

module.exports = router;

And here is how your routes file should look like if you want to use TypeScript:

routes/users.ts
import * as express from 'express';
import { PermissionMiddlewareCreator } from 'forest-express-sequelize';
import users from '../interfaces/users';

const router = express.Router();
const permissionMiddlewareCreator = new PermissionMiddlewareCreator('users');

// Create a User
router.post('/users', permissionMiddlewareCreator.create(), (request, response, next) => {
  next();
});

// Update a User
router.put('/users/:recordId', permissionMiddlewareCreator.update(), (request, response, next) => {
  next();
});

// Delete a User
router.delete('/users/:recordId', permissionMiddlewareCreator.delete(), (request, response, next) => {
  // Learn what this route does here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/routes/default-routes#delete-a-record
  next();
});

...

// Delete a list of Users
router.delete('/users', permissionMiddlewareCreator.delete(), (request, response, next) => {
  next();
});

export default router;

Directory: /forest

There is nothing special to do here. Translating your smart configuration (actions, fields, and segments) in .ts is as simple as just renaming the file and updating your imports.

Let's take the following example:

forest/users.js
const { collection } = require('forest-express-sequelize');

collection('users', {
  actions: [{
    name: "Promote to admin",
    type: 'single',
  }],
  fields: [],
  segments: [],
});

Translating this file into TypeScript will give the following:

forest/users.ts
import { collection } from 'forest-express-sequelize';

collection('users', {
  actions: [{
    name: "Upgrade to admin",
    type: 'single',
  }],
  fields: [],
  segments: [],
});

Debugging your application

If you want to ease debugging sessions on runtime, you can ask TypeScript to keep a link between your .ts and .js files by using .js.map files. These files are source map files, that let debugging tools map between the emitted JavaScript code and the TypeScript source files that created it. Many debuggers (e.g. Visual Studio, Chrome's dev tools...) can consume these files so you can debug the TypeScript file instead of the JavaScript file. You can ask TypeScript to generate those files by changing the configuration with the following:

tsconfig.json
{
  "compilerOptions": {
    ...
    "sourceMap": true,
    ...
  },
  ...
}

Now that your .js.map files are generated, we need to prevent your generated project from considering .js.map files under your models directory as models. To do it, reach models/index.js and change the following original code:

models/index.js
...
fs
  .readdirSync(__dirname)
  .filter(function (file) {
    return (file.indexOf('.') !== 0) && (file !== 'index.js');
  })
  .forEach(function (file) {
    try {
      var model = sequelize['import'](path.join(__dirname, file));
      db[model.name] = model;
    } catch (error) {
      console.error('Model creation error: ' + error);
    }
  });
...

with this:

models/index.js
...
fs
  .readdirSync(__dirname)
  .filter(function (file) {
    return (file.indexOf('.') !== 0) && (file !== 'index.js') && file.indexOf('.js.map') === -1;
  })
  .forEach(function (file) {
    try {
      var model = sequelize['import'](path.join(__dirname, file));
      db[model.name] = model;
    } catch (error) {
      console.error('Model creation error: ' + error);
    }
  });
...

We now filter actual model files correctly by not taking .js.map files into account.

Last updated