Throw away the "Script" from "Type""Script".
Tags: typescript, javascript, webdev, discuss
Published: 2023-09-16 14:03:01

Before writing this article, I will first attach a post that was posted on a community a few days ago.
[Turbo 8 is dropping TypeScript](https://world.hey.com/dhh/turbo-8-is-dropping-typescript-70165c01)
After seeing the title of the above article, I thought it was about the library turbo8 migrating from TypeScript to JSDoc.
However, it was not.
The team removed the type annotations from their codebase, changed the file extensions to js, but did not add comments.
Yes, what they wanted was freedom from types.
At this point, I will attach the post that was posted the day after the above post.
[Open source hooliganism and the TypeScript meltdown](https://world.hey.com/dhh/open-source-hooliganism-and-the-typescript-meltdown-a474bfda)


## TypeScript is not productive.

I mostly agree with DHH's post, and I think that some of the older JavaScript-based libraries that support `@types` are supporting TypeScript simply because they don't have the courage to stand up to the TypeScript zealots.
There are many cases where libraries support `@types` because TypeScript users demand it, even if they think TypeScript is unproductive.
However, I am very positive about types.
I am a fan of the JSDoc tool, to be exact.
Therefore, in this article, I will introduce how convenient JSDoc is.


## JSDoc's type is just a comments.

JSDoc is a tool that provides type hints for JavaScript using comments.
Some TypeScript programmers criticize this, arguing that comments should only be comments and should not affect code.
However, TypeScript does not have runtime type checking, and JSDoc types are only used for IntelliSense, linting, and code summarization.
Therefore, comments are a very appropriate place to write types.
JSDoc does not affect the source code in any way. It also skips the unnecessary compilation step of TypeScript, allowing you to enjoy the freedom of JavaScript to the fullest.
Of course, there are a few exceptions.
For example, if the type is incorrectly declared or [shadowing](https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing) is attempted, I had to use `@ts-ignore` to work around the issue.
I have never studied TypeScript separately, so there may be other elegant solutions that I am not aware of.
```ts
/// lib.es5.d.ts
charCodeAt(index: number): number;
```
```js
/// js-source
// @ts-ignore: index -> index?
const code = str.charCodeAt()
```
```js
/** @type {RegExp} */// @ts-ignore: string -> RegExp
let regex = ".[:=]."
// @ts-ignore: string -> RegExp
for (let key of map.keys())  += "|" + key
regex = RE(regex)
```


## Do types make JavaScript harder?

Let's reverse the question.
How can types make JavaScript difficult?
Could it be a long learning curve, adding verbose syntax, or difficult types?
Here, it seems that the only thing that can be established for JSDoc is difficult types.
I had no problem using JSDoc even with a basic knowledge of Java and a quick glance at a Svelte codebase written in JSDoc.

### 1. How to set up JSDoc

For more information, please see my article titled [How to configure JSDoc instead of TypeScript](https://dev.to/artxe2/how-to-set-up-jsdoc-for-npm-packages-1jm1).
I have made several improvements to make it possible to apply JSDoc to library code concisely and to use it in both JS and TS projects.

### 2. Applying types to code with JSDoc

This is also all covered in the [TypeScript - JSDoc Reference](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html).
The important points can be summarized as follows.
```js
/**
 * @template T
 * @param {T|import("../../private.js").SomeType} my_parameter
 * @returns {Map<T, number>}
 */
function my_func(my_parameter) {
  /** @type {[T, number][]} */
  const array = [
    [/** @type {T} */(my_parameter), 1]
  ]
  const my_map = new Map(array)
  return my_map
}
```

TypeScript has excellent type inference, so JSDoc only requires a few additional syntaxes.
Here is an example of my actual code.
```js
/**
 * @param {Map<*[], Function>} nodes
 * @param {number} index
 */
const run_dag = (nodes, index) =>
  new Promise(
    (resolve, reject) => {
      /** @type {Map<*[], Function>} */
      const jobs = new Map()
      /** @type {Map<Function, *[][]>} */
      const dependents = new Map()
      /** @type {[number]} */
      const count = [ nodes.size ]

      for (const [dependencies, callback] of nodes) {
        const clone = [...dependencies]
        jobs.set(clone, callback)
        for (const p of clone)
          if (typeof p == "function") {
            const array = dependents.get(p)
            if (array) array.push(clone)
            else dependents.set(p, [ clone ])
          }
      }
      for (const [dependencies, callback] of jobs)
        if (
          dependencies.every(p => typeof p != "function")
        ) {
          run_node(resolve, reject, jobs, dependents, count, dependencies, callback, index)
            .catch(reject)
        }
    }
  )
```
JSDoc is used in the above code to specify the types of parameters, the generic type of a Map object, and the length of a number[] used as a pointer.
The rest of the code is typed with type inference.
JSDoc also supports auto-completion, making it easy to write function parameters.
![JSDoc auto-completion](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ni9tru9w93q39f0lvf2p.gif)


## How to write complex and challenging types

So, there is one reason left why we should not use JSDoc.
The reason why DHH said "Things that should be easy become hard, and things that are hard become `any`. No thanks!" is probably because defining complex and challenging types is literally complex and challenging.
Adding types to JavaScript provides several improvements to the developer experience, such as autocompletion support and better syntax highlighting.
However, in the case of complex types, typing can be cumbersome, and features like type checking may not work properly.
I will use the types of eslint that I recently worked on as an example.
```ts
export interface Property extends BaseNode {
    type: 'Property';
    key: Expression | PrivateIdentifier;
    value: Expression | Pattern; // Could be an AssignmentProperty
    kind: 'init' | 'get' | 'set';
    method: boolean;
    shorthand: boolean;
    computed: boolean;
}
export interface NodeMap {
    AssignmentProperty: AssignmentProperty;
    CatchClause: CatchClause;
    ...
    Property: Property;
    ...
}
type Node = NodeMap[keyof NodeMap];
interface Identifier extends BaseNode, BaseExpression, BasePattern {
    type: 'Identifier';
    name: string;
}
interface NodeParentExtension {
    parent: Node;
}
Identifier: (node: Identifier & NodeParentExtension) => void
```
```js
Identifier(node) {
  const parent = node.parent
  const type = parent.type
  if ("Property" == type) {
    if (parent.value == node) {
      if (parent.key.name == parent.value.name) { // TS error here
...
}
```
eslint's `Rule.RuleListener.Identifier` generates a lot of type errors in normal JS code, making autocompletion impossible.
This is despite the incredibly verbose and detailed `d.ts` files of eslint and estree.
My initial solution was to make things that are hard become `any`, as shown below.
```ts
/// private.d.ts
interface ASTNode extends Node, Pattern {
  computed: boolean
  id: ASTNode
  imported: ASTNode
  key: ASTNode
  left: ASTNode
  local: ASTNode
  name: string
  object: ASTNode
  parent: ASTNode
  property: ASTNode
  range: [number, number]
  right: ASTNode
  shorthand: boolean
  type: string
  value: ASTNode
}
```
```js
/** @param {import("../private").ASTNode} node */
Identifier(node) {
...
}
```
It was a "No Thanks" solution, but I finished coding by fixing type errors and getting some Intellisense support.
After that, I considered how to use the verbose types of estree, and the result is as follows.
```ts
/// private.d.ts
export type ASTNode = {
  end: number
  parent: ASTNode
  range: [number, number]
  start: number
} & (
  estree.ArrayExpression
  | estree.ArrayPattern
  | estree.ArrowFunctionExpression
  | estree.AssignmentExpression
  | estree.AssignmentPattern
  | estree.AwaitExpression
  | estree.BinaryExpression
  | estree.BlockStatement
  | estree.BreakStatement
  | estree.CallExpression
  | estree.CatchClause
  | estree.ChainExpression
  | estree.ClassBody
  | estree.ClassDeclaration
  | estree.ClassExpression
  | estree.ConditionalExpression
  | estree.ContinueStatement
  | estree.DebuggerStatement
  | estree.DoWhileStatement
  | estree.EmptyStatement
  | estree.ExportAllDeclaration
  | estree.ExportDefaultDeclaration
  | estree.ExportNamedDeclaration
  | estree.ExportSpecifier
  | estree.ExpressionStatement
  | estree.ForInStatement
  | estree.ForOfStatement
  | estree.ForStatement
  | estree.FunctionDeclaration
  | estree.FunctionExpression
  | estree.Identifier
  | estree.IfStatement
  | estree.ImportDeclaration
  | estree.ImportDefaultSpecifier
  | estree.ImportExpression
  | estree.ImportNamespaceSpecifier
  | estree.ImportSpecifier
  | estree.LabeledStatement
  | estree.Literal
  | estree.LogicalExpression
  | estree.MemberExpression
  | estree.MetaProperty
  | estree.MethodDefinition
  | estree.NewExpression
  | estree.ObjectExpression
  | estree.ObjectPattern
  | estree.PrivateIdentifier
  | estree.Program
  | estree.Property & { key: estree.Identifier }
  | estree.PropertyDefinition
  | estree.RestElement
  | estree.ReturnStatement
  | estree.SequenceExpression
  | estree.SpreadElement
  | estree.StaticBlock
  | estree.Super
  | estree.SwitchCase
  | estree.SwitchStatement
  | estree.TaggedTemplateExpression
  | estree.TemplateElement
  | estree.TemplateLiteral
  | estree.ThisExpression
  | estree.ThrowStatement
  | estree.TryStatement
  | estree.UnaryExpression
  | estree.UpdateExpression
  | estree.VariableDeclaration
  | estree.VariableDeclarator
  | estree.WhileStatement
  | estree.WithStatement
  | estree.YieldExpression
)
```
```js
/** @param {import("../private").ASTNode & import("estree").Identifier} node */
Identifier(node) {
...
}
```
The second solution works amazingly well, and I was able to find and fix errors in my first solution's code using typecheck.
The type error was caused by incorrect NodeMap, Identifier, and NodeParentExtension types.
I think that almost all of the unpleasantness of using JSDoc comes from incorrect type declarations.
However, I think it would be difficult to have incorrect type declarations if you coded using JSDoc from the beginning.
This is because **you can use JS code as a type directly.**


## Getting the most out of type inference

JavaScript has implicit types, which allow TypeScript to perform type inference.
We can use JavaScript's basic types without declaring separate types.
```js
export const number = 0 // number
export const string_array = ["string"] // string[]
export const object = { key: "Hello" } // { key: string }
export const map = new Map // Map<any, any>
export class Class {} // class Class
export function func() { return new Set } // function (): Set<any>
```
Of course, it is also possible to use the inferred types from JavaScript.
```ts
import * as js from "./export.js"

typeof js.number // number
typeof js.string_array // string[]
typeof js.object // { key: string }
typeof js.map // Map<any, any>
new js.Class // Class
typeof js.func // function (): Set<any>
ReturnType<typeof js.func> // Set<any>
```
As you can see from the above examples, TypeScript is able to infer the complex types that occur in JavaScript, so we can simply use them.


# Conclusion

Are you using TypeScript?
Use JSDoc.
Are you using JavaScript?
Use JSDoc.
Does TypeScript's unnecessary compilation harm the developer experience?
Use JSDoc.
Does JavaScript's lack of IntelliSense harm the developer experience?
Use JSDoc.
Does TypeScript's typecheck harm JavaScript's freedom?
Make use of type inference.
Is it cumbersome to write d.ts to support TypeScript for a JavaScript library?
Use JSDoc to generate it automatically.

Thank you.