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?
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 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.
You can document TypeScript with JSDoc syntax—the 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.
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.
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
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.
{
"compilerOptions": {
"allowJs": true, "noEmit": true }
}
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.
@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)
}
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.
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) {
// ...
}
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
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
@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.
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.
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.
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?
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)
// ...
}
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?
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) => {
// ...
}
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.
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:
@template
@returns
@returns
@callback
@enum
Consult the official JSDoc in TypeScript documentation for details on these features and more.
Next: Let's recap what we've learned.
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) {
// ...
}
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 */
Use the equal sign to denote nullable types. This is equivalent to User | null | undefined
.
/** @param {User=} user */
Use @type
to document parameters of an anonymous function.
numbers.map((/** @type number */ n) => { return n * 2
})
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
}