How to add Typescript to the Django project

Table of Contents

Objectives

In this tutorial, I'd like to talk about Typescript and after reading, you will learn:

  1. JavaScript is an interpreted language and the pain it might bring to us
  2. What is Typescript and what problem it can help us solve
  3. How to add Typescript to the frontend project in Django (without touching React, Vue or other heavy frontend frameworks)
  4. The solution provided in this tutorial can also work with Flask, another web framework in the Python community.

Background

JavaScript is an interpreted language, not a compiled language. A program such as C++ or Java needs to be compiled before it is run. The source code is passed through a program called a compiler, which translates it into bytecode that the machine understands and can execute. In contrast, JavaScript has no compilation step. Instead, an interpreter in the browser reads over the JavaScript code, interprets each line, and runs it. More modern browsers use a technology known as Just-In-Time (JIT) compilation, which compiles JavaScript to executable bytecode just as it is about to run.

Let's assume you wrote a function foo, if developer pass in the wrong type of arguments (age here), the function will still run (without raising exception), but the result is not what you expected.

function foo(name, age) {
  console.log(`Hello ${name}, after 2 years, your age will be ${age + 2}`);
}

foo('michael', 30);
// Hello michael, after 2 years, your age will be 32

foo('michael', '30');
// Hello michael, after 2 years, your age will be 302

You can add comments to the foo function, so developer can know what type of arguments he can pass in, or you can convert the string type to int type, but the solutions are still not good enough.

And the bug is not easy to detect if your frontend project is big.

Is there any better way to solve this problem?

How about this:

function foo(name: string, age: number) {
  console.log(`Hello ${name}, after 2 years, your age will be ${age + 2}`);
}

foo('michael', '30');

Notes:

  1. With name: string, age: number, we defined the arguments type. (Typing Annotations)
  2. Then we can use some tool to help us do static type checking and help us find the bug before we run the code.

Actually, Python already supports this feature, and it's called type hints, and you can check Support for type hints to learn more.

What is TypeScript

For now, Javascript does not support the Type Annotations feature (https://github.com/tc39/proposal-type-annotations), so we need to use TypeScript to help us do the job.

TypeScript is a free and open source high-level programming language developed and maintained by Microsoft. It is a strict syntactical superset of JavaScript and adds optional static typing to the language.

Notes:

  1. Javascript is an interpreted language, and it does not need a compilation step.
  2. TypeScript, however, it needs a compilation step.
  3. TypeScript compiler (tsc) will do type checking, and compile the TypeScript to Javascript code, which can run on the browser.

For example, below is the sample TypeScript code:

// app.ts

function foo(name: string, age: number) {
    console.log(`Hello ${name}, after 2 years, your age will be ${age + 2}`);
}

After compilation, it will look like this in the final built js file, and the Typing Annotations are removed since they are not needed anymore.

// app.js

function foo(name, age) {
    console.log("Hello ".concat(name, ", after 2 years, your age will be ").concat(age + 2));
}

Can TypeScript Work with Existing Projects?

Yes, you do not need to rewrite your project to make it work.

Even for a legacy project which is written in jQuery, you can still add TypeScript to it.

  1. If you want to add new business logic, you can create *.ts files and write your code in TypeScript.
  2. Or you can rename your existing *.js files to *.ts, and add Typing Annotations to the existing code.
  3. Using TypeScript does not mean you need to use React, Vue or other heavy frontend frameworks, you can still use jQuery, Bootstrap, or any other frontend libraries.

Pre-requisite

For Python developers, I'd like to introduce a boilerplate project which contains all the frontend tools, and you can use it to create your own frontend project within minutes.

https://github.com/AccordBox/python-webpack-boilerplate

This tool can work with Django, Flask smoothly.

Step 1: Install TypeScript

After we create the frontend project using python manage.py webpack_init from https://github.com/AccordBox/python-webpack-boilerplate, let's go to the directory which contains package.json, run command to install TypeScript

$ npm install --save-dev typescript ts-loader

Step 2: Config TypeScript

Create config file next to the package.json, and name it tsconfig.json, which is config file for TypeScript compiler

{
  "compilerOptions": {
    "strictNullChecks": true,
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "moduleResolution": "node"
  },
  "include": ["./src"]
}

The include property specifies the files to be included in the TypeScript project, you might need to change it to your own project path.

.
├── package.json
├── src
├── tsconfig.json
└── webpack

Update frontend/webpack/webpack.common.js

const getEntryObject = () => {
  const entries = {};
  // for javascript entry file
  glob.sync(Path.join(__dirname, "../src/application/*.js")).forEach((path) => {
    const name = Path.basename(path, ".js");
    entries[name] = path;
  });
  // for typescript entry file
  glob.sync(Path.join(__dirname, "../src/application/*.ts")).forEach((path) => {
    const name = Path.basename(path, ".ts");
    entries[name] = path;
  });
  return entries;
};

module.exports = {
  resolve: {
    extensions: [".ts", ".js"],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },

      ...
    ],
  },
};

Notes:

  1. In the getEntryObject function, we add some code to handle *.ts entry files.
  2. In the resolve property, we add .ts to the extensions array, so we can import *.ts files without adding the extension.
  3. In the rules property, we add a new rule to handle *.ts files.

Change frontend/src/application/app.js to frontend/src/application/app.ts to make it has *.ts suffix.

// add foo function
function foo(name: string, age: number) {
    console.log(`Hello ${name}, after 2 years, your age will be ${age + 2}`);
}

Here we added a foo function which has Typing Annotations

Let's run the project and see what will happen

$ npm run watch

./src/application/app.ts 531 bytes [built]
webpack 5.70.0 compiled successfully in 38 ms

Let's do a test, if we call foo function with wrong parameter type foo('michael','12') in the app.ts

We will get

TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

As you can see, the TypeScript compiler will do type checking for us, and we can find the bug before we deploy the code on server.

Step 3: Code Linting

ESLint is a static code analysis tool for identifying problematic patterns found in JavaScript code.

We use ESLint to help us check our Javascript code and detect potential problem.

To check TypeScript, we will use typescript-eslint

$ npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin

If we check package.json, we can see the packages already installed there.

Update .eslintrc

{
  "parser": "@babel/eslint-parser",
  "extends": [
    "eslint:recommended"
  ],
  "env": {
    "browser": true,
    "node": true
  },
  "parserOptions": {
    "ecmaVersion": 8,
    "sourceType": "module",
    "requireConfigFile": false
  },
  "rules": {
    "semi": 2
  },
  // for typescript
  "overrides": [
    {
      "files": "**/*.+(ts|tsx)",
      "parser": "@typescript-eslint/parser",
      "parserOptions": {
        "project": "./tsconfig.json"
      },
      "plugins": ["@typescript-eslint"],
      "extends": [
        "plugin:@typescript-eslint/eslint-recommended",  // removes redundant warnings between TS & ESLint
        "plugin:@typescript-eslint/recommended"          // rules specific to typescript, e.g., writing interfaces
      ]
    }
  ]
}

Notes:

  1. We add overrides property to handle *.ts files.
  2. This can make eslint work for both Javascript and Typescript in our frontend project.
# let's check our code
$ npx eslint ./src

15:10  warning  'foo' is defined but never used  @typescript-eslint/no-unused-vars

As you can see, we get a warning from typescript-eslint, which reminds us that we have a function foo which is defined but never used.

Eslint with Webpack

Update frontend/webpack/webpack.config.watch.js and frontend/webpack/webpack.config.dev.js

new ESLintPlugin({
  ...
  extensions: ["js", "ts"],
}),

For ESLintPlugin, we add ts to the extensions array, so it can check both *.js and *.ts files.

Now if we run npm run watch or npm run dev, we will get the same result as we run npx eslint ./src

Webpack  -> ESLintPlugin -> Eslint -> Typescript-eslint for *.ts and Eslint for *.js

Step 4: Code Formatting

To format our Typescript code, we will use Prettier

Please check https://github.com/pre-commit/mirrors-prettier and install it with pre-commit

TypeScript Declaration File For 3-rd Party Libraries

The "*.d.ts" file is a TypeScript file that contains type declarations for JavaScript libraries.

For example, you wrote a Javascript lib jQuery many years ago, now you want to use it in a Typescript project, you can write a *.d.ts file to declare the type of jQuery.

Actually, some people already did that, and you can find many *.d.ts files for you to use in DefinitelyTyped

For example, for jQuery, you can install

$ npm install --save @types/jquery

The type declaration files would be placed under node_modules/@types folder, by default (set by moduleResolution in tsconfig.json), Typescript will look for *.d.ts files in node_modules/@types folder.

Custom Declaration File

Let's add below code to the frontend/src/application/app.ts

window.AccordBox = {
    hello: function () {
        console.log('hello from AccordBox');
    }
};

We added a AccordBox object to the global window object, and we want to use it in other Typescript files via window.Accordbox

If we build using Webpack, we got below error

TS2339: Property 'AccordBox' does not exist on type 'Window & typeof globalThis'.

The Window interface is defined in the lib.dom.d.ts of TypeScript, since it does not have the AccordBox property, that is why we got the above error.

We can extend the Window interface to solve this issue

Create frontend/src/types/window.d.ts

interface Window {
    // Allow us to put arbitrary objects in window
    /* eslint-disable @typescript-eslint/no-explicit-any */
    [key: string]: any;

    // someProperty: SomeType;
}

You can also explicitly declare the property if you like.

Now if we build using Webpack, the error will be resolved.

Conclusion

Typescript is a great tool which can help us to write better frontend code. It can work better with IDE code intellisense, and improve the productivity of frontend development.

You do not need to rewrite your whole frontend project, you can just start with a small part, and gradually migrate to Typescript step by step.

References

Launch Products Faster with Django

Unlock the power of Django combined with Hotwire through SaaS Hammer. Supercharge productivity, tap into Python's rich ecosystem, and focus on perfecting your product!

Michael Yin

Michael Yin

Michael is a Full Stack Developer who loves writing code, tutorials about Django, and modern frontend techs.

He has published some tech course on testdriven.io and ebooks on leanpub.

© 2024 SaaS Hammer