Skip to main content

2 posts tagged with "JSDoc"

View All Tags

ยท 6 min read

There's a debate to be had about whether using JavaScript or TypeScript leads to better outcomes when building a project. The introduction of using JSDoc annotations to type a JavaScript codebase introduces a new dynamic to this discussion. This post will investigate what that looks like, and come to an (opinionated) conclusion.

title image reading "JSDoc JavaScript vs TypeScript" with a JavaScript logo and TypeScript logo

This blog is evolving into meetup talk. Join us on Dec 1st at 2pm EDT / 7pm GMT - sign up here

If you'd talked to me in 2018, I would have solidly recommended using TypeScript, and steering away from JavaScript. The rationale is simple: I'm exceedingly convinced of the value that static typing provides in terms of productivity / avoiding bugs in production. I appreciate this can be a contentious issue, but that is my settled opinion on the subject. Other opinions are available.

TypeScript has long had a good static typing story. JavaScript is dynamically typed and so historically has not. Thanks to TypeScript support for JSDoc, JavaScript can now be statically type checked.

What is JSDoc JavaScript?

JSDoc itself actually dates way back to 1999. According to the Wikipedia entry:

JSDoc is a markup language used to annotate JavaScript source code files. Using comments containing JSDoc, programmers can add documentation describing the application programming interface of the code they're creating.

The TypeScript team have taken JSDoc support and run with it. You can now use a variant of JSDoc annotations to provide type information in JavaScript files.

What does this look like? Well, to take a simple example, a TypeScript statement like so:

let myString: string;

Could become the equivalent JavaScript statement with a JSDoc annotation:

/** @type {string} */
let myString;

This is type enhanced JavaScript which the TypeScript compiler can understand and type check.

Why use JSDoc JavaScript?

Why would you use JSDoc JavaScript instead of TypeScript? Well there's a number of possible use cases.

Perhaps you're writing simple node scripts and you'd like a little type safety to avoid mistakes. Or perhaps you want to dip your project's toe in the waters of static type checking but without fully committing. JSDoc allows for that. Or perhaps your team simply prefers not having a compile step.

That, in fact, was the rationale of the webpack team. A little bit of history: webpack has always been a JavaScript codebase. As the codebase grew and grew, there was often discussion about using static typing. However, having a compilation step wasn't desired.

TypeScript had been quietly adding support for type checking JavaScript with the assistance of JSDoc for some time. Initial support arrived with the --checkJs compiler option in TypeScript 2.3.

A community member by the name of Mohsen Azimi experimentally started out using this approach to type check the webpack codebase. His PR ended up being a test case that helped improve the type checking of JavaScript by TypeScript. TypeScript v2.9 shipped with a whole host of JSDoc improvements as a consequence of the webpack work. Being such a widely used project this also helped popularise the approach of using JSDoc to type check JavaScript codebases. It demonstrated that this approach could work on a significantly sized codebase.

These days, JSDoc type checking with TypeScript is extremely powerful. Whilst not quite on par with TypeScript (not all TypeScript syntax is supported in JSDoc) the gap in functionality is pretty small.

It's a completely legitimate choice to build a JavaScript codebase with all the benefits of static typing.

Why use TypeScript?

So if you were starting a project today, and you'd decided you wanted to make use of static typing, how do you choose? TypeScript or JavaScript with JSDoc?

Well, unless you've a compelling need to avoid a compilation step, I'm going to suggest that TypeScript may be the better choice for a number of reasons.

Firstly, the tooling support for using TypeScript directly is better than that for JSDoc JavaScript. At the time of writing, things like refactoring tools etc in your editor work more effectively with TypeScript than with JSDoc JavaScript. (Although these are improving as time goes by.)

Secondly, working with JSDoc is distinctly "noisier". It requires far more keystrokes to achieve the same level of type safety. Consider the following TypeScript:

function stringsStringStrings(
p1: string,
p2?: string,
p3?: string,
p4 = 'test'
): string {
// ...
}

As compared to the equivalent JSDoc JavaScript:

/**
* @param {string} p1
* @param {string=} p2
* @param {string} [p3]
* @param {string} [p4="test"]
* @return {string}
*/
function stringsStringStrings(p1, p2, p3, p4) {
// ...
}

It may be my own familiarity with TypeScript speaking, but I find that the TypeScript is easier to read and comprehend as compared to the JSDoc JavaScript alternative. The fact that all JSDoc annotations live in comments, rather than directly in syntax, makes it harder to follow. (It certainly doesn't help that many VS Code themes present comments in a very faint colour.)

My final reason for favouring TypeScript comes down to falling into the "pit of success". You're cutting against the grain when it comes to static typing and JavaScript. You can have it, but you have to work that bit harder to ensure that you have statically typed code. On the other hand, you're cutting with the grain when it comes to static typing and TypeScript. You have to work hard to opt out of static typing. The TypeScript defaults tend towards static typing, whilst the JavaScript defaults tend away.

As someone who very much favours static typing, you can imagine how this is compelling to me!

It's your choice!

So in a way, I don't feel super strongly whether people use JavaScript or TypeScript. But having static typing will likely be a benefit to new projects. Bottom line, I'm keen that people fall into the "pit of success", so my recommendation for a new project would be TypeScript.

I really like JSDoc myself, and will often use it on small projects. It's a fantastic addition to TypeScript's capabilities. For bigger projects, I'll likely go with TypeScript from the get go. But really, this is a choice - and either is great.

This post was originally published on LogRocket.

ยท 15 min read

Days of Yore

It was my first job. The web was alive and well at this point but still very much in it's infancy. Newspapers had only recently moved on from calling it "the information superhighway". No-one was doing real programming for the web - the desktop was where it was at.

As for me, I was writing call centre software. It was all very exciting. Here was the idea: the phone on your desk would start ringing and through the magic of TAPI our app would be presented with the telephone number of the dialer. It would then look up that telephone number in the appropriate CRM application and pop the callers details on the screen. You'd pick up the phone and bellow "why hello Mr Jones!" and either impress the caller with your incredible fore-knowledge of who had rung you or perhaps terrify them with our Brave New Orwellian World.

My job was to work out how to call into the APIs of the various CRM applications / databases being used and extract the relevant information. So it goes without saying that I have spent a lot of time with badly documented APIs. Or in fact undocumented APIs. I know pain my friend...

Hours and days were spent debugging and walking APIs just to find out what they could do and what information they exposed. This, I need hardly say, was dull and tedious work. Having spent longer than I care to remember with no more information on an API than method names has left its mark on me. I am consequently keener than your average dev on documentation and intellisense. When you've stared at the coalface of the Lotus Notes API for 2 weeks with only Dephi 3 as your constant companion you'd feel the same way too. (This was before the days of Google and actually being able to find stuff on the internet.)

If you can convey information about the API that you're building then I'd say you're duty-bound to do so. Or at least that it's good manners.

Definitely Intellisensed

When I started getting involved with the Definitely Typed project my focus was on giving good Intellisense. Where there was documentation for an API I wanted to get that popping in front of users when they hit the "." key:

As the above screenshot demonstrates TypeScript supports Intellisense through a slightly tweaked implementation of JSDoc:

With 0.8.2, the TypeScript compiler and tools now support JSDoc comments.

In the TypeScript implementation, because types are already part of the system, we allow the JSDoc type annotation to be elided, as in the example above.

You can now document a variety of language constructs (including classes, modules, interfaces, and functions) with comments that become part of the information displayed to the user. Weโ€™ve also started extending lib.d.ts, the default JS and DOM API library, with JSDoc comments.

Partly as an exercise in getting better acquainted with TypeScript and partly responding to my instinctive need to have nicely documented APIs I decided to start adding JSDoc comments to the world's most popular typings file <a href="https://github.com/borisyankov/DefinitelyTyped/blob/master/jquery/jquery.d.ts">jquery.d.ts</a>.

Why jquery.d.ts?

Well a number of reasons:

  1. I used jquery.d.ts already myself and I'm a firm believer in eating your own dogfood
  2. jQuery is well documented. I needed a source of information to power my JSDoc and api.jquery.com had my back.
  3. jquery.d.ts was widely used. Given how ubiquitous jQuery has become this typing file was unsurprisingly the most popular in the world. That was key for me as I wanted feedback - if I was making a mess of the typings I wanted someone to pitch in and tell me.

Just to digress once more, points #2 and #3 turned out to be of particular note.

Concerning point #2, I did find the occasional error or inconsistency in the jQuery API documentation. These were definitely the exception rather than the rule though. And thanks to the very helpful Dave Methvin these actually lead to minor improvements to the jQuery API documentation.

Concerning point #3 I did indeed get feedback. As well as enriching jquery.d.ts with JSDoc goodness I also found myself fixing slight errors in the typings. Here and there I would find examples where jquery.d.ts was out of line the with API documentation. Where this was the case I would amend the typings to bring them into line - trying to make jquery.d.ts entirely API-compliant. This was not always popular. But despite the heat it generated I think it ended up leading to a better typing file. I'm again grateful for Dave Methvin's thoughtful contributions.

Turning API documentation into JSDoc

I wanted to take an example of API documentation and demonstrate how that can be applied to a typing file with particular focus on how JSDoc comments can be created to drive Intellisense. So let's take everyone's favourite jQuery method: val. The documentation of val can be found here: api.jquery.com/val

By the way, check out the *entirely* intuitive URL. Now you've clocked just how straightforward that is you've probably a fair idea how you could find pretty much any jQuery documentation you might need without recourse to Google. Brilliant!

Let's take a look at what val looked like before JSDoc in the first version of the typing available on GitHub. (By the way, remember the original jquery.d.ts came out of the TypeScript team):

val(): any;
val(value: string[]): JQuery;
val(value: string): JQuery;
val(value: number): JQuery;
val(func: (index: any, value: any) => any): JQuery;

And now let's look at jquery.d.tsafter JSDoc:

/**
* Get the current value of the first element in the set of matched elements.
*/
val(): any;
/**
* Set the value of each element in the set of matched elements.
*
* @param value A string of text or an array of strings corresponding to the value of each matched element to set as selected/checked.
*/
val(value: string): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param value A string of text or an array of strings corresponding to the value of each matched element to set as selected/checked.
*/
val(value: string[]): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments.
*/
val(func: (index: number, value: string) => string): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments.
*/
val(func: (index: number, value: string[]) => string): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments.
*/
val(func: (index: number, value: number) => string): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments.
*/
val(func: (index: number, value: string) => string[]): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments.
*/
val(func: (index: number, value: string[]) => string[]): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments.
*/
val(func: (index: number, value: number) => string[]): JQuery;

Many changes yes? Let's break it down a little.

1. You have 20 seconds to comply (with the API)

The first thing to note is the number setter method:

val(value: number): JQuery;

Let's have a look at the jQuery documentation for the simple setter:

.val( value )

value
Type: String or Array
A string of text or an array of strings corresponding to the value of each matched element to set as selected/checked.

See the problem? There is *no* number setter. The typings are wrong. So let's remedy this:

<strike>val(value: number): JQuery;</strike>

2. String and Array of String setters

The documentation states that we have setters which accept String and Array of String. These are already modeled in the existing typings by the string and string[] overloads:

val(value: string[]): JQuery;
val(value: string): JQuery;

So let's enrich these typings with some JSDoc:

/**
* Set the value of each element in the set of matched elements.
*
* @param value A string of text or an array of strings corresponding to the value of each matched element to set as selected/checked.
*/
val(value: string): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param value A string of text or an array of strings corresponding to the value of each matched element to set as selected/checked.
*/
val(value: string[]): JQuery;

If you look you can see we've added a related JSDoc style comment block prior to each overload. The first part of the comment ("Set the value of...") is the overarching Intellisense that is displayed. Each of the @param statements represents each of the parameters and it's associated comment. By comparing the API documentation to the JSDoc it's pretty clear how the API has been transformed into useful JSDoc.

It's worth noting that I could have taken the choice to customise the @param value comments based on the overload I was JSDoc-ing. Arguably it would have been more useful to have something like this instead:

/**
* Set the value of each element in the set of matched elements.
*
* @param value A string of text <strike>or an array of strings</strike> corresponding to the value of each matched element to set as selected/checked.
*/
val(value: string): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param value <strike>A string of text or</strike> an array of strings corresponding to the value of each matched element to set as selected/checked.
*/
val(value: string[]): JQuery;

After some pondering I decided not to take this approach, just to maintain that close relationship between jquery.d.ts and api.jquery.com. It's open to debate how useful that relationship actually is so I thought I'd just highlight this as a choice I made.

3. Getter

The jQuery documentation for the getter looks like this:

[

.val()

](http://api.jquery.com/val/#val)Returns: String or Number or Array

Description: Get the current value of the first element in the set of matched elements.

So the val() overload can return a string, a number or a string[]. Unfortunately there is no real way to model that in TypeScript at present due to the absence of "union types". Union types are being discussed at present but in TypeScript v1.0 world the only viable approach is returning the any type. This implies val() returns any possible JavaScript value from boolean to Function and straight on 'til morning. So clearly this isn't accurate but importantly it also allows for the possibility of val() returning string, number or string[].

The final getter typing with JSDoc applied ends up looking like this:

/**
* Get the current value of the first element in the set of matched elements.
*/
val(): any;

As you can see the "Get the current value..." from the API docs has been used as the overarching Intellisense that is displayed for the getter.

4. The Function setter

Finally we're going to take a look at the Function setter which is documented as follows:

[

.val( function(index, value) )](http://api.jquery.com/val/#val-functionindex--value)

function(index, value)
Type: Function()
A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments.

If you cast your eyes back to the original typings for the Function setter you'll see they look like this:

val(func: (index: any, value: any) => any): JQuery;

This is a good start but it's less accurate than it could be in a number of ways:

  1. index is a number - we needn't keep it as an any
  2. value is the old value - we know from our getter that this can be a string, number or string[]. So we can lose the any in favour of overloads which specify different types for value in each.
  3. The return value of the function is the value that should be set. We know from our other setters that the possible types allowed here are string and string[]. (And yes I'm as puzzled as you are that the getter can return a number but the setter can't set one.) That being the case it makes sense for us to have overloads with functions that return both string and string[]

So, we've got a little tidy up to do for #1 and extra overloads to add for #2 and #3. We're going to replace the single Function setter with 3 overloads to cater for #2. Then for #3 we're going to take each of the 3 overloads we've just created and make 2 overloads place of each to handle the different return types. This will lead us with the grand total of 6 overloads to model our Function setter!

/**
* Set the value of each element in the set of matched elements.
*
* @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments.
*/
val(func: (index: number, value: string) => string): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments.
*/
val(func: (index: number, value: string[]) => string): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments.
*/
val(func: (index: number, value: number) => string): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments.
*/
val(func: (index: number, value: string) => string[]): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments.
*/
val(func: (index: number, value: string[]) => string[]): JQuery;
/**
* Set the value of each element in the set of matched elements.
*
* @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments.
*/
val(func: (index: number, value: number) => string[]): JQuery;

A cursory glance shows that each of the overloads above shares the same JSDoc. Each has the "Set the value..." from the API docs as the overarching Intellisense that is displayed for the Function setter. And each has the same @param func comment as well.

It could be you...

This post is much longer than I ever intended it to be. But I wanted to show how easy it is to create typings with JSDoc to drive Intellisense. For no obvious reason people generally don't make a great deal of use of JSDoc when creating typings. Perhaps the creators have no good source of documentation (a common problem). Or perhaps people are not even aware it's a possibility - they don't know about the TypeScript support of JSDoc. In case it's the latter I think this post was worth writing.