A Guide for Replacing TypeScript

Jordan BrennanJordan Brennan
6 min read

The following steps will help you remove TypeScript from your project and replace it with JSDoc and native JavaScript features. As a result, your IDE will provide more helpful info, the code will be more robust, the app bundle will be smaller, and the project setup and build time will be faster and easier to manage.

Phase One: Add JSDoc

TypeScript code is not removed during this phase and the project is expected to continue to build as usual. Adding JSDoc to a project requires no setup whatsoever. You can immediately start adding JSDoc comments to any project and it just works (most IDEs will even auto-generate the stubs for you).

  1. Add JSDoc @typedef and/or @interface annotations above the declarations in your declaration files (*.d.ts) and other type definitions files you might have. For example:

     // BEFORE
     type User = {
       name: string;
       email: string;
       role: 'parent' | 'student';
       accountId: string;
       isLoggedIn?: boolean;
     }
    
     // AFTER
     /**
      * @typedef {object} User
      * A user of the system. This is different from an {@link Account}.
      * @prop {string} name - User's name.
      * @prop {string} email - User's email (also their username).
      * @prop {'parent'|'student'} role - User's role. See {@link http://your-tech-wiki.com/roles} to learn more.
      * @prop {string} accountId - The {@link Account} this User belongs to.
      * @prop {boolean} [isLoggedIn=false] - User is authenticated.
      */
     type User = {
       name: string;
       email: string;
       role: 'parent' | 'student';
       accountId: string;
       isLoggedIn?: boolean;
     }
    
  2. Add JSDoc @typedef and/or @interface to other types and interfaces defined throughout your files. I find it's best to move all these into top-level types files, but do whatever makes sense for your project.

  3. Add JSDoc annotations to functions and variable declarations, for example:

// BEFORE
const message: string = 'Hello World!';
const user: User = {};

function isAdult(age: number): boolean {
  return age >= 18;
}

// AFTER
const message: string = 'Hello World!'; // It's obvious, do nothing.
/** @type User */
const user: User = {};

/**
 * Returns true if `age` is greater than or equal to 18.
 * @param {number} age
 * @returns {boolean}
*/
function isAdult(age: number): boolean {
  return age >= 18;
}

At this point, all types have a JSDoc equivalent and your IDE should be providing this info through its intellisense.

Phase Two: Use Native JavaScript Features

There are many features in JavaScript that enable you to check types and ensure the integrity of your code. Here's my reference list of 27 of those features. Use them as much as possible.

  1. Use classes

    • They're self-documenting

    • They have tons of features: static, public, and private fields, encapsulation, extendable, etc. They're very powerful!

    • But be intentional - not everything needs to be a class

        class Car {
          model = '';
          year = 0;
          #serialNumber = ''; // # = private
      
          constructor(model, year, serialNumber) {
            this.model = model;
            this.year = year;
            this.#serialNumber = serialNumber;
          }
      
          get serialNumber() {
            return this.#serialNumber;
          }
        }
      
  2. Do type-checking when needed

     // Use typeof to check primitive types
     if (typeof callback === 'function') {
       callback()
     }
    
     // Use instanceof to check constructor
     if (color instanceof Color) {
       setPaintBrush(color)  
     }
    
     // But don't overdo it.
     // For example, don't check emailRegex because String.match's 
     // arg is "implicitly converted to a RegExp".
     if (email.match(emailRegex)) {
       send(email, 'Hi')
     }
    
  3. Use optional chaining liberally

    • According to Rollbar, the most common JavaScript runtime error is accessing an undefined property. Optional chaining prevents this error.

        const c = a.b?.c; // If b is not guaranteed
      
  4. Validate inputs

    • TypeScript can't guarantee runtime safety, so validating inputs would be necessary even with TypeScript.

    • JavaScript has lots of ways to validate values.

  5.      // BEFORE
         function isAdult(age: number): boolean {
           return age >= 18;
         }
    
         // AFTER
         /**
          * Returns true if `age` is greater than or equal to 18.
          * @param {number} age
          * @returns {boolean}
         */
         function isAdult(age: number): boolean {
           const val = parseInt(age);
           return Number.isInteger(val) && val >= 18;
         }
    
  6. Use default values liberally

    • Default function paraments help guard against bad inputs

    • Default assignment helps guard against undefined properties or variables

    • One option is a Proxy that provides defaults for undefined properties

        // Default function parameter
        function isAdult(age: number = 0): boolean {
          ...
        }
      
        // Default on assignment
        const message = msg || 'Hi';
        const count = num ??= 0;
      
        // Default when destructuring
        const {rating = 0} = response.data;
      
        // Default via Proxy handler (assume target is response from above)
        const handler = {
          get: function(target, prop) {
            if (prop == 'rating') {
              return target.data?.rating ? target.data.rating : 0
            }
            return target[prop]
          }
        });
      
        const {rating} = response.data;
      

At this point, your project is now using more JavaScript to achieve runtime safety - which is what matters most. JavaScript has lots of features to help you.

Phase Three: Remove TypeScript Code

Leave the compiler alone for now. As you remove TypeScript from your source code you will see less and less errors from the compiler until it finds none. That's when you know you've removed all your TypeScript code.

  1. Remove all the proprietary syntax and keywords from functions and variables

    • Between JSDoc and using JavaScript, this stuff no longer serves a purpose.

    • Source code becomes less noisy and arguably easier to read.

        // BEFORE
        const message: string = 'Hello World!';
        const user: User = {};
      
        function isAdult(age: number): boolean {
          return age >= 18;
        }
      
        // AFTER
        const message = 'Hello World!';
        /** @type User */
        const user = {};
      
        /**
         * Returns true if `age` is greater than or equal to 18.
         * @param {number} age **WITH A DEFAULT THIS IS INFERRED. CAN BE REMOVED.**
         * @returns {boolean}
         */
        function isAdult(age = 0) {
          const val = parseInt(age);
          return Number.isInteger(val) && val >= 18;
        }
      
  2. Remove the type and interface definitions throughout the codebase (if you didn't previously move them to common top-level declaration files).

  3. Rename the declaration files with a .js extension. I use typedefs.js at the project root. The type definitions are now available project-wide and your IDE will work its magic from here on out. Replace other TypeScript file extensions with .js (or .jsx).

At this point your code no longer uses TypeScript. The last thing to do is remove it from the project.

Phase Four: Remove TypeScript Dependency

  1. Remove TypeScript dependencies from your package.json

  2. Delete all TypeScript and TypeScript-related config

  3. Build your project to verify changes

  4. Check around for other things that should be updated to reflect this change (tech stack documentation, instructions in the README file, .gitignore, etc.).

  5. Optionally, explore additional JSDoc features like @event and @listens to document events, @link to navigate to other types or to a website, @namespace for really large projects, and more.

Now that you're done, the project has an even more useful IDE experience, a faster and simpler build, more robust execution at runtime where it really matters, and a smaller bundle for the browser to download and execute. Congratulations!

Oh, one more thing. The final undocumented phase is to not be intimidated by other developers who are going to criticize you. Show them the zero-config setup. Show them the faster build time. Have them try the improved IDE experience. Have them feel the freedom to work without the hassle of constantly trying to keep the type checker happy while you narrow in on a final solution. Open the Dev Tools and show off the smaller bundle size and the built code that's easier to debug. Tell them how you have personally changed your focus to writing runtime-safe JavaScript rather than leaning on a successful build as be the bar of quality.

2
Subscribe to my newsletter

Read articles from Jordan Brennan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jordan Brennan
Jordan Brennan

Frontend engineer