Today I learned

Type annotations in JavaScript files

Is it possible to get the benefits of type checking without TypeScript's syntax? Absolutely

TypeScript lets you annotate your JavaScript with type annotations. It can even check these for errors in build-time, so you can catch errors before they get deployed to production. You'll never have to deal with another undefined is not a function error ever again!

TypeScript, by default, requires you to make a few changes to your build setup. You'll need to rename your JavaScript files to .ts and .tsx, and either use tsc (the TypeScript Compiler) or Babel (with preset-typescript) to compile them.

Next: Why not just use the TypeScript syntax?

TypeScript syntax

Working with TypeScript means having to use a new syntax, even if it's a strict superset of JavaScript. Many developers don't like having to deal with a new syntax. If this describes you, then this article is for you.

typescript-syntax-example.ts
/*
 * TypeScript syntax allows you to put inline type annotations...
 * but it's not really JavaScript anymore.
 */

function repeat(text: string, count: number) {  return Array(count + 1).join(text)
}
Next: Let's learn about an alternative to the TypeScript syntax.

The JSDoc syntax

Documenting JavaScript

You can document TypeScript with JSDoc syntaxthe standard syntax for documenting JavaScript. While JSDoc is primarily used as a means of writing documentation, TypeScript can read JSDoc's type annotations.

/**
 * Repeats some text a given number of times.
 *
 * @param {string} text - The text to repeat * @param {number} count - Number of times */

function repeat(text, count) {
  return Array(count + 1).join(text)
}

This means you can take advantage of TypeScript's type checking in JavaScript, without having to convert your JavaScript code to TypeScript.

Why JSDoc?

It's a great idea to use JSDoc whether you use TypeScript or not. It's the de facto standard for documenting JavaScript, and is supported by a lot of tools and editors.

If you're using JSDoc to document your JavaScript, you might as well let TypeScript enforce the integrity of your types in your code.

Next: Let's set up TypeScript to use JSDoc.

TypeScript setup

We'll need TypeScript to get started. Install the typescript npm package in your project to get started.

npm install typescript
# -or-
yarn add typescript

Enable JSDoc type checking

Configure TypeScript to check your JavaScript files. (By default, TypeScript only checks .ts files.) TypeScript is configured using the tsconfig.json file. We'll also be using the noEmit option, since we're only going to be using TypeScript as a type checker.

tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,    "noEmit": true  }
}

Try it

Run tsc to check your project's types. It's recommended to add this to your CI, too, so you can automatically enforce it in your project's changes.

./node_modules/.bin/tsc
# -or-
yarn run tsc
Next: Let's document our code with JSDoc.

Basic annotations

Function parameters

Use @param to document types of a function's parameters. You'll need to put these in JSDoc comments, which are block comments that begin with two stars.
/**
 * @param {string} text * @param {number} count */

function repeat(text, count) {
  return Array(count + 1).join(text)
}

Documenting code

JSDoc is, first and foremost, a documentation tool. Aside from adding type annotations, you might as well use it to document what your functions do.

/**
 * Repeats a given string a certain number of times.
 *
 * @param {string} text - Text to repeat * @param {number} count - Number of times */

function repeat(text, count) {
  return Array(count + 1).join(text)
}

Here's the same example, but with some text to describe what it does.

Next: Let's write some more annotations.

Documenting parameters

Optional types

Add an equal sign at the end of a type to signify that it's optional. In this example, number= is the same as number | null | undefined. This special syntax ("closure syntax") is only available in JSDoc types.
/**
 * @param {string} text
 * @param {number=} count */

function repeat(text, count = 1) {
  // ...
}

Documenting options

You can document properties of params, like options.count and options.separator in this example. You can use this to document React props in function components, too!

/**
 * @param {string} text - Text to repeat
 * @param {Object} options * @param {number} options.count * @param {string} options.separator */

function repeat(text, options) {
  console.log(options.count)
  console.log(options.separator)
  // ...
}
repeat('hello', { count: 2, separator: '-' })
Next: Let's write some more annotations.

Type assertions

Variables

Use @type to provide inline types to variable declarations. This isn't typically needed for constants, as TypeScript can usually infer types pretty well. It's a great fit for non-constant variables, though (ie, let).
/**
 * Time out in seconds.
 * @type number */

let timeout = 3000

Function parameters

@type can also be used to provide inline type definitions to function arguments. Great for anonymous functions.

list.reduce((
  /** @type number */ acc,  /** @type number */ item) => {
  return acc + item
}, 0)
Next: Let's refactor our type definitions to be in external files.

Importing definitions

Importing types

Complex, reusable types are better defined in an external TypeScript file. You can then import these TypeScript definitions into your JavaScript files.

/** @typedef { import('./myTypes').User } User */
/**
 * @param {User} author
 */

function cite(author) {
  // ...
}

Import types using the special import syntax. You can then define your types in an external .d.ts file.

Defining types externally

Define your types in an ambient definition file (.d.ts). Note that these files need to be TypeScript files; there's no way to export type definitions from a .js file.

myTypes.d.ts
export interface User {
  name: string
  email: string
}

Using import(), the JSDoc syntax effectively is as feature-rich as the TypeScript syntax. If the JSDoc syntax is too limiting, you can define your types in a TypeScript file and import them later.

Next: Can I define complex types in JavaScript files?

Type definitions in JavaScript

Object types

Use @typedef to define a type. External .d.ts files are preferred to this approach, but this syntax is available should you need it.

/**
 * @typedef {Object} Props * @property {string} title - The title of the page * @property {number} updatedAt - Last updated time */

/**
 * A component.
 *
 * @param {Props} props */

const ArticleLink = (props) => {
  console.log(props.title)
  console.log(props.updatedAt)
  // ...
}

Union types

Use union types (|) to signify types that can be one or another. To simplify things, you can define a typedef for them.

/** @typedef {number | string} NumberOrString */
Next: What about React?

Using with React

Function components

Function components are plain functions. You can document them in any of the ways we previously learned to document functions. In this example, we'll document them using object types.

/**
 * This is a React function component.
 *
 * @param {Object} props * @param {string} props.title * @param {string} props.url * @param {string} props.image */

const ArticleLink = (props) => {
  // ...
}

Class components

Use @extends to define the types for your props and state. You can then use @typedef (either inline or imports) to define what Props and State are.

/**
 * This is a React class component.
 *
 * @extends {React.Component<Props, State>} */

class MyComponent extends React.Component {
  // ...
}
Next: Let's write some more annotations.

Advanced features

The JSDoc syntax isn't as expressive as the TypeScript syntax, but it comes very close. There are also some other advanced TypeScript features that are available in JSDoc:

  • Templates with @template
  • Return values with @returns
  • Type guards with @returns
  • Function types with @callback
  • Enums with @enum
  • ...and more

Consult the official JSDoc in TypeScript documentation for details on these features and more.

Next: Let's recap what we've learned.

Recap

Documenting functions

Write your documentations as block comments that begin with a double-star. Document parameters with @param.

/**
 * Multiply a number by itself.
 * @param {number} n - What to square */

function square(n) {
  // ...
}

Importing type definitions

Import type definitions with @typedef and import. This allows you to write your type definitions in TypeScript ambient definition files (.d.ts).

/** @typedef { import('./myTypes').User } User */

Optionals

Use the equal sign to denote nullable types. This is equivalent to User | null | undefined.

/** @param {User=} user */

Anonymous functions

Use @type to document parameters of an anonymous function.

numbers.map((/** @type number */ n) => {  return n * 2
})

Documenting options

You can document the properties of object parameters.

/**
 * @param {Object} options * @param {number} options.count * @param {string} options.sep */

function repeat(options) {
  // ... options.count, options.sep
}

You have just read Type annotations in JavaScript files, written on April 07, 2019. This is Today I Learned, a collection of random tidbits I've learned through my day-to-day web development work. I'm Rico Sta. Cruz, @rstacruz on GitHub (and Twitter!).

← More articles