DEV Community

Cover image for Throw away the "Script" from "Type""Script".
Yeom suyun
Yeom suyun

Posted on • Edited on

Throw away the "Script" from "Type""Script".

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;
Enter fullscreen mode Exit fullscreen mode
/// js-source
// @ts-ignore: index -> index?
const code = str.charCodeAt()
Enter fullscreen mode Exit fullscreen mode
/** @type {RegExp} */// @ts-ignore: string -> RegExp
let regex = ".[:=]."
// @ts-ignore: string -> RegExp
for (let key of map.keys())  += "|" + key
regex = RE(regex)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
  )
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode
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
...
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
/** @param {import("../private").ASTNode} node */
Identifier(node) {
...
}
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode
/** @param {import("../private").ASTNode & import("estree").Identifier} node */
Identifier(node) {
...
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
overflow profile image
overFlow

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?

Collapse
 
tohodo profile image
Tommy • Edited

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 any for everything) that it defeats the point of using TS 🤦‍♂️

Collapse
 
artxe2 profile image
Yeom suyun

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 any is certainly overused for things that are not any.

Collapse
 
stgogm profile image
Santiago "Momo" Marín

And who will keep the JSDocs up to date?

Collapse
 
efpage profile image
Eckehard • Edited

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.

Collapse
 
artxe2 profile image
Yeom suyun • Edited

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.

Collapse
 
efpage profile image
Eckehard

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.

Collapse
 
abhishekvash profile image
Abhishek S

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.

Collapse
 
pengeszikra profile image
Peter Vivo • Edited

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.

import { FC } from "react";
import { useStateFactory } from "react-state-factory";
import { reducer, initialState, labels } from "@foo/reducer";


export const FooComponent:FC = () => {
  const [state, put] = useStateFactory(reducer, initialState, labels);

  // state and put types defined by user with initialState and labels state
  // in this example one of put actions is put.INSERT_CONTENT(content: number)
  // VSCode or any typescript friendly editor help to list all aviable commands, 
  // with proper parameter(s) type.

  return (
    <section>
      <button onClick={() => put.INSERT_CONTENT(Date.now())}></button>
      <pre>{JSON.stringify(state, null, 2)</pre>
   </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
ctsstc profile image
Cody Swartz • Edited

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.

Collapse
 
lassazvegaz profile image
LassazVegaz

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…

Collapse
 
fuzzhd profile image
Justin Condello

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.

Collapse
 
howesteve profile image
Steve Howe

Let's see you write something like zoo or typeirm with plain js.