TypeScript VS JSDoc: Which One Has a Better Cost-Performance Ratio?

Is TypeScript unaffordable or does JSDoc have a better cost-performance ratio?

1. Is TS not fragrant anymore?

In 2023, several news about Typescript broke the silence and brought some excitement to the front-end development community that had little new activity.

First, GitHub’s report stated: “TypeScript replaces Java as the third most popular language.”

In its annual Octoverse Open Source Report, TypeScript has become increasingly popular in the most popular programming languages. For the first time, it has surpassed Java to become the third most popular language among OSS projects on GitHub, with a user growth of 37%.

According to Stack Overflow’s Developer Survey Report for 2023, JavaScript has been the most popular programming language for 11 consecutive years, with a usage rate of 63.61%. TypeScript ranks fifth with a usage rate of 38.87%.

The bigger controversy comes from In September 2023, DHH, the creator of Ruby on Rails, announced the removal of TypeScript code from Turbo 8, their team’s open-source project.

He thinks that TypeScript is just a hindrance to him. Not only because it requires explicit compilation steps, but also because it pollutes the code with type programming, which greatly affects the development experience.

Coincidentally, not long ago, the well-known front-end UI framework Svelte announced its switch from TypeScript to JavaScript. The developer responsible for the Svelte compiler said that after switching to JSDoc, the code can be debugged without the need for compilation and build processes — simplifying the development work of the compiler.

Svelte is not the first front-end framework to abandon TypeScript. As early as 2020, Deno migrated some of its internal TypeScript code to JavaScript to reduce build time.

As a result, several projects have already switched from TypeScript to JavaScript in a short period this year, which is quite confusing. Has switching back from TypeScript to JavaScript become a trend? Isn’t this going against historical progress?

TypeScript

TypeScript, released by Microsoft in 2012, is positioned as a superset of JavaScript. Its capabilities are based on the ECMAScript specification formulated by TC39 (i.e., JavaScript). The industry started using TypeScript because it provides type checking to address the issue of JavaScript’s lack of types and only having logic.

For large projects, collaborative work, and projects requiring high reliability, using TypeScript is a good choice. The main benefits of static type checking include:

  • Type safety
  • Code intelligent perception
  • Refactor support

The main problems brought by TS are:

  • The core code of certain libraries is very small, but the type of gymnastics brings several times the cost of learning, development, and maintenance.
  • TypeScript compilation speed is slow, and implementations like build currently do not support decorators and other features.
  • The compilation size will increase due to various repetitive redundant definitions and utility methods.

Unlike Svelte developers who abandoned TS events because of their annoyance, the JSDoc they switched to is a familiar stranger to many developers.

2. JSDoc: Do I look like before?

As early as 1999, a prototype of JSDoc with a similar syntax to Javadoc appeared in Rhino, a JS engine written in Java released by Netscape/Mozilla.

Michael Mathews officially launched the JSDoc project in 2001 and released version 1.0 in 2007. It wasn’t until 2011 that the refactored JSDoc 3.0 could run on Node.js.

JSDoc syntax examples

Define object type:

/**
 * @typedef {object} Rgb
 * @property {number} red
 * @property {number} green
 * @property {number} blue
 */

/** @type {Rgb} */
const color = { red: 255, green: 255, blue: 255 };

Define function type:

/**
 * @callback Add
 * @param {number} x
 * @param {number} y
 * @returns {number}
 */
const add = (x, y) => x + y;

Define enumeration:

/**
 * Enumerate values type
 * @enum {number}
 */
const Status = {
  on: 1,
  off: 0,
};

Define class:

class Computer {
  /**
   * @readonly Readonly property
   * @type {string}
   */
  CPU;

  /**
   * @private Private property
   */
  _clock = 3.999;

  /**
   * @param {string} cpu
   * @param {number} clock
   */
  constructor(cpu, clock) {
    this.CPU = cpu;
    this._clock = clock;
  }
}

In practice, it is often used in conjunction with tools such as jsdoc2md to automatically generate API documentation for libraries.

With the popularity of the development paradigm of separating front-end and back-end, the complexity of front-end business logic is also increasing. Although there is no need to generate external API documentation for every application, type safety has become increasingly important, and developers have started to use JSDoc in their business projects. However, TypeScript, which was born shortly after that, quickly took over this process.

However, the inherent problems of TS mentioned earlier also troubled developers until several landmark events happened this year that brought everyone’s attention back to JSDoc. People were surprised to find that JSDoc did not stay in the old times.

In addition to the continuous enrichment of JSDoc itself, the release of TypeScript 2.9 in 2018 is undoubtedly the most surprising boost; this version fully supports defining JSDoc type declarations in TS style and also supports dynamically importing and resolving TS types in JSDoc comments’ type declarations.

For example, some type definitions mentioned in the previous text can be written in this new syntax as follows:

Define object type:

/**
 * @typedef {{ brand: string; color: Rgb }} Car
 */

/** @type {Rgb} */
const color = { red: 255, green: 255, blue: 255 };

Define function type:

/**
 * @typedef {(x: number, y: number) => number} TsAdd
 */

/** @type {TsAdd} */
const add = (x, y) => x + y;

Union types in TS can also be used directly:

/**
 * Union type with the pipe operator
 * @typedef {Date | string | number} MixDate
 */

/**
 * @param {MixDate} date
 * @returns {void}
 */
function showcase(date) {
  // date is Date
  if (date instance of Date) date;
  // date is a string
  else if (type of date === 'string') date;
  // date is the number
  else date;
}

The generic type is also fine.

/**
 * @template T
 * @param {T} data
 * @returns {Promise<T>}
 * @example signature:
 * function toPromise<T>(data: T): Promise<T>
 */
function toPromise(data) {
  return Promise.resolve(data);
}

/**
 * Restrict template by types
 * @template {string|number|symbol} T
 * @template Y
 * @param {T} key
 * @param {Y} value
 * @returns {{ [K in T]: Y }}
 * @example signature:
 * function toObject<T extends string | number | symbol, Y>(key: T, value: Y): { [K in T]: Y; }
 */
function object(key, value) {
  return { [key]: value };
}

Type guard:

/**
 * @param {any} value
 * @return {value is YOUR_TYPE}
 */
function isYourType(value) {
 let type;
 /**
  * Do some kind of logical testing here
  * - Always return a boolean
  */
 return type;
}

As for dynamically importing TS definitions, it is very simple. Regardless of whether the project itself supports TS, we can confidently and boldly define the type definition in a .d.ts file first, such as:

// color.d.ts
export interface Rgb {
  red: number;
  green: number;
  Blue: number;
}

export interface Rgba extends Rgb {
  alpha: number;
}

export type Color = Rgb | Rbga | string;

Then in JSDoc:

// color.js 
/** @type {import('<PATH_TO_D_TS>/color').Color} */
const color = { red: 255, green: 255, blue: 255, alpha: 0.1 };

Of course, their support can enhance type safety for IDEs with built-in JSDoc-based type-checking tools, such as the representative VSCode. The corresponding TS types to JSDoc types (even without using TS syntax) will be automatically inferred and displayed. After configuring //@ts-check, it can display real-time type errors like a TS project. These are all easy to imagine and will not be elaborated here.

The integration of JSDoc and TS capabilities means the simplification and modernization of the former’s writing style, becoming a convenient bridge to TS. It also provides an opportunity for the latter to seamlessly integrate into most existing pure JS projects at zero cost. This path has suddenly become much wider.

3. Example: Progressive replacement of Protobuf+TS

Since we have found a way to enable regular JS projects to quickly access type checking, it is worth considering the benefits that can be replicated from TS for projects that will never or may not be refactored into TS in the short term.

For most modern frontend and backend-separated projects, a major pain point is the disconnect between core business knowledge in the front end and back end. Frontend and backend developers understand business logic based on PRD or UI, then summarize entities, enumerations, and data derivation logic specific to their projects; these are also referred to as domain knowledge or metadata. The disconnect of these elements in frontend projects manifests as a series of problems:

  • The input and response types of the API data interface are unclear.
  • Many default values for form items need to be hard-coded and maintained in multiple places.
  • The naming of variables or actions for the same concept is inconsistent between the front-end and back-end.
  • Mocking needs to be handwritten and often does not match the actual data structure in the end.
  • There is a lack of basis for TDD, making code refactoring difficult. Lack of intelligent awareness and prompts in VSCode.

For the above problem, the ideal solution is for the front-end team to also handle the development of the Node.js middleware BFF. This way, both organization and technology can be maximally shared.

  • But from many recent practices in the industry, it is undoubtedly difficult to achieve: even if the front-end team has the ability and willingness, this BFF mode is also difficult to sustain. There are problems with the Node.js technology stack facing complex business challenges, as well as natural resistance from existing backend teams.
  • A successful and well-received solution for both front-end and back-end integration is ProtoBuf, developed by Google.

In usual circumstances, the design concept of ProtoBuf (Protocol Buffers) is to first define a .proto file and then use a compiler to generate corresponding code (such as Java classes and d.ts type definitions). This approach ensures consistency in data structures across different languages and provides cross-language capabilities for data serialization and deserialization.

  • However, this undoubtedly requires both the front-end and back-end teams to change their development methods simultaneously. If it is not a project starting from scratch, there may be some difficulty in promoting it.

Therefore, combining the ability of JSDoc, we can design a transformation plan that is second best but not far away — under the premise of requiring the backend team to write relatively organized entity definitions and so on, write extraction and conversion scripts, regularly or manually generate corresponding JSDoc type definitions, thus achieving accurate synchronization of frontend and backend business logic.

For example, taking a Java BFF project as an example, the following conversion can be done.

Enumeration:

public enum Color {
    RED("#FF0000"), GREEN("#00FF00"), BLUE("#0000FF");

    private String hex code;

    Color(String hexCode) {
        this.hexCode = hexCode;
    }

    public String getHexCode() {
        return hex code;
    }
}

public enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

Convert to:

/**
 * @readonly
 * @enum {String}
 */
export const Color = {
  RED: '#FF0000',
  GREEN: '#00FF00',
  BLUE: '#0000FF',
}

/**
 * @readonly
 * @enum {Number}
 */
export const Day = {
  MONDAY: 0,
  TUESDAY: 1,
  WEDNESDAY: 2,
  THURSDAY: 3,
  FRIDAY: 4,
  SATURDAY: 5,
}

POJO:

public class MyPojo {
 private Integer id;
 private String name;

 public Integer getId() {
  return id;
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }
}

Convert to:

/**
 * @typedef {Object} MyPojo
 * @property {Integer}  [id]
 * @property {String}  [name]
*/

In terms of conversion methods, theoretically, it would be better to use means such as AST, but in the case of Java in this example, there doesn’t seem to be a particularly mature conversion tool available and there is also a lack of documentation on libraries like java-parser.

On the other hand, although regex-based conversion is more closely coupled with specific backend writing styles, it can still be considered simple and flexible. And there we have it. I hope you have found this useful. Thank you for reading.

Continue Learning

Discover more articles on similar topics