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
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
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 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.
/// lib.es5.d.ts
charCodeAt(index: number): number;
/// js-source
// @ts-ignore: index -> index?
const code = str.charCodeAt()
/** @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.
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.
The important points can be summarized as follows.
/**
* @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.
/**
* @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.

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.
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
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.
/// 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
}
/** @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.
/// 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
)
/** @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.
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.
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.
Latest comments (91)
before i even read it further. i am a proponent of learning one thing and one thing only. and that thing is JS. i went through a Typescript lesson and i kinda felt like people are just creating languages just for the fun of it and making reasons that do not make sense to me...
typescript is JavaScript. What do the javascript guys got to say perhaps for copyright infringement.
I was thinking the other day:"What if the JS guys institute and ability for Types in JS if its not already available. Then what of typeScript?
What doesn't get mentioned enough is the potential linting bottlenecks in large TS projects inside VS Code, which makes DX counterproductive and unpleasant. Here's an example -- imagine waiting a minute to get rid of squigglies while the linting panel jumps all over the place every time you make a code change:
user-images.githubusercontent.com/...
There are plenty of other examples. You can blame a bad acting npm package or whatever, but sometimes you have no control and other times you end up wasting time (and hair) trying to troubleshoot linting performance issues instead of actual coding.
Another observation from years involved with TS projects on agency side: TS has its benefits in reducing the number of bugs and hinting, but so many developers are using it inappropriately (e.g., going away from defaults by relaxing rules overzealously or using
anyfor everything) that it defeats the point of using TS 🤦♂️I agree with the points made. However, based on my research, I believe that the performance issues are caused by the misuse of ESLint plugins.
While it is always a problem to misuse tools, TS's
anyis certainly overused for things that are notany.And who will keep the JSDocs up to date?
We should not forget, that missing types are not the only thing that distinguishes Javscript from a compiled language. If you write some code in C++ or Pascal, all parts of the code are visited during compilation. So, the compiler is able to find errors that are hidden somewhere in the code, even if this code is not actually executed. Same is true for tree shaking, that can be done by the Linker automatically.
In Javascript, things are different. Many errors stay hidden somewhere in the code, until it is finally executed. So, the ability to do code checks is limited anyway. Typescript may help a bit, but the result is still different from what we have been used to.
So for me the effort using typescript does not pay back most of the time. Adding some more type checks to the code helps, to keep things running.
I believe that there are many people, including you, who believe that pure JavaScript is more productive than TypeScript.
TypeScript's type checking may be seen as unnecessary if you have a well-written testing process.
However, JSDoc has the advantage of providing a go to source definition feature for TypeScript users.
This allows users to verify that library code is safe and is a great help for debugging and contributing if a bug occurs.
No, that´s not the point. I hate to know that my code is full of hidden errors and can break anytime. It´s more a feeling that Typescript does not solve the problem in the same way, a full featured compiled language did. But this is not typescripts fault, it is more a price you pay, if you use an interpreted language.
I believe JSDoc is not really a silver bullet. Typescript isn’t either, but for the most part, it’s a lot more usable than JSDoc. For one, it’s cleaner. JSDoc makes me retch. And two, I think it’s easier to have less experienced developers adopt TS over JSDoc since it resembles strongly typed languages they would’ve used before. And as for build times? I think developer time is more expensive than machine build times, and in that sense, time wasted writing verbose comments in JSDoc costs the company more than what a TS compilation does.
I wouldn't think that the problem that can be implemented with Typescript can be solved with JSDoc. Specifically, how a function declared with user-defined state and action union types, returns a collection of actions equipped with dispatch, which assigns the appropriate types to the appropriate labels. This provides a type-safe useReducer solution for programmers.
About this npm module: react-state-factory
Thus, working with JSDoc in this way may entail too much additional effort and make the codebase much more difficult to maintain.
Why not instead use something like Flow? Why not use a build tool that skips type checking?
Typescript used to be about so much more than just types. It used to provide a feature set on top of JS that it was missing such as decorators, classes, private and public accessors to methods, and more. JS has caught up quite a bit over the years, but still isn't great in some of the features.
I would still much rather write a class in TS that looks closer to C# than write one in plain JS right now, but it is possible now days so I appreciate that. I would still rather have keywords like private and public rather than prefix with # for private access.
There are now ways to spin up Typescript projects effortlessly, or utilize TS first runtimes like Deno or Bun. I just don't get why so many are ready to abandon ship, and why others are acting like JS doc ran through a TS language server is "new" this feature has been part of VSCode for quite some time. It used to be a feature you had to enable first or maybe your still do? The irony is that it's still using TS as the language server, but it's more isolated and without a large transpilation step.
Things have come a long way, but I too am not super happy with the whole ecosystem of transpiling through to Jest and the pains of ESM. Hopefully runtimes like Deno and Bun inspire a better Node ecosystem, or maybe it's time to embrace them.
Adding types as comments is productive than TS? Funny
People will miss TS again when the deployed code crashes because x is not a property of undefined and y is not a function and etc…
I was doing code conversion from Java to JavaScript. I had the opportunity to use TypeScript instead but I couldn't wrap my head around it. Then, when using IntelliJ, I learned about JSDoc. At that point, I just fed all the functions and classes for my project to ChatGPT to generate the JSDoc. I touched up some of the comments from GPT to expand out some descriptions. And like magic, IntelliJ started to show much more useful hints and made coding much easier.
Let's see you write something like zoo or typeirm with plain js.