Typescript for Strapi! Life’s too short to ES5!

Intro

Objective

Step 1: rewrite your *.js to *.ts

Step 1.1: A place for our shiny new Typescript code

Step 1.2: Some boilerplate

declare namespace strapi {
const config;
const plugins;
const services;
const utils;
}

Step 1.3: (Optional) Generating interfaces from models

"gen-ts-interfaces": "sts api/ -o ts/models/"

Step 1.4: Rewriting controllers

npm i --save-dev tsyringe
{
"method": "GET",
"path": "/confirm_email",
"handler": "UserProfile.confirm",
"config": {
"policies": []
}
}
...
const mailService = require('../../message/services/Mail.service');
...
module.exports = {
confirm: async (ctx) => {
... // the body of the function
}
}
...
import 'reflect-metadata';
import { autoInjectable } from 'tsyringe';
import { MailService } from '../../message/services/Mail.service'; // serves purely as injection example@autoInjectable() // informs tsyringe that this class needs to have its constructor members injected automatically
class UserProfileController {
constructor(
...
private mailService?: MailService, // example: this is how we inject a service
) {}
...
async confirm(ctx) {
... // the body of the function
}
}
module.exports = new UserProfileController(); // VERY IMPORTANT!

Advice from my refactoring experience

import { MailService } from '../../message/services/Mail.service';

Step 1.5: Rewriting everthing else

Step 1.6: (Optional) Bonus: rewriting tricky policy functions

const userProfileService = require('../../UserProfile.service');module.exports = async (ctx, next) => {
const user = await userProfileService.getUserByFacebookSenderId(ctx.request.header['facebooksenderid']);
...
}
import { autoInjectable } from 'tsyringe';
import { UserProfileService } from '../../services/UserProfile.service';
@autoInjectable()
export class IsKnownSenderService {
constructor(private userProfileService?: UserProfileService) {
}
async isKnownSender(ctx, next) {
const user = await this.userProfileService.getUserByFacebookSenderId(ctx.request.header['facebooksenderid'];
...
}
}
const isKnownSenderService = new IsKnownSenderService();
module.exports = isKnownSenderService.isKnownSender.bind(isKnownSenderService);

Step 1.7: (Optional) Bonus: should I convert everything to Typescript?

Step 1.8: Gitignoring & removing generated code

Step 2: Set up the Typescript transpiler

Step 2.1: Make sure you have typescript installed:

npm i --save-dev typescript

Step 2.2: Create the tsconfig.json config file for the Typescript transpiler.

{
"compilerOptions": {
"outDir": "./api",
"target": "es5",
"declaration": true,
"removeComments": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"sourceMap": true,
"noImplicitAny": false
},
"include": [
"./api_ts/**/*"
]
}

Step 2.3: Transpile

"build-ts": "tsc",
"watch-ts": "tsc -w"

Step 3: Workaround for Strapi permissions

'use strict';/**
* An asynchronous bootstrap function that runs before
* your application gets started.
*
* This gives you an opportunity to set up your data model,
* run jobs, or perform some special logic.
*
* See more details here: https://strapi.io/documentation/3.0.0-beta.x/configurations/configurations.html#bootstrap
*/
const find = require('lodash/find');
const fs = require('fs');
const fixPermissionsForTypescriptControllers = async () => {
const userPermService = strapi.plugins['users-permissions'].services['userspermissions'];
const roles = await userPermService.getRoles();
fs.readdir('api', (err, controllerDirs) => { // iterating controllers
controllerDirs.forEach((controller) => {
const controllerRoutesPath = `api/${controller}/config/routes.json`;
if (fs.existsSync(controllerRoutesPath)) {
const controllerRoutes = require(`../../${controllerRoutesPath}`)['routes']; // getting the route config
controllerRoutes.forEach((controllerRoute) => { // using the special "roles" & "enabled" fields
if (controllerRoute['roles']) {
const action = controllerRoute['handler'].substring(controllerRoute['handler'].indexOf('.') + 1);
controllerRoute['roles'].forEach(async (roleName) => {
const role = find(roles, { name: roleName });
// manually updating the permissions table to include those methods
await strapi.query('permission', 'users-permissions').model.updateOne(
{
action,
controller,
role: role['_id'],
type: 'application',
},
{ enabled: controllerRoute['enabled'], policy: '' },
{ upsert: true },
);
});
}
});
}
});
});
};
module.exports = async () => {
await fixPermissionsForTypescriptControllers();
};

Important note

(Optional) Step 4: Debugging in Typescript FTW

const strapi = require('strapi');
strapi({ dir: process.cwd(), autoReload: true }).start();
"debug": "nodemon --inspect=0.0.0.0:9229 server.js",
"watch-debug": "concurrently --handle-input \"npm run watch-ts\" \"npm run debug\" "

Closing thoughts

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Alexandru Constantin

Software & Machine Learning Engineer. I love building products that deliver value & happiness! https://constantin.al