A simple, but non-trivial example of getting the most from JSDoc + tsserver (Type Linting without TypeScript)
If you'd like to get the benefits of Type Linting without drinking the TypeScript Kool-Aid, you're in the right place.
This project has purposefully half-baked - meaning that errors exist on purpose (with explanatory comments) - so that you can see the type linting in action!
You get benefits from both explicit and implicit type linting.
Here are some of the extra checks tsserver
will give you that you wouldn't get
from jshint
alone:
require
ing a file that doesn't existundefined
propertyif { ... }
conditionnull
Even if you don't use JSDoc (or TypeScript Definitions) to add explicit types, you still get the benefits of code analysis.
If you do add JSDoc type annotations to your JavaScript, you'll get even more specific errors.
git
node
typescript
(and jshint
)vim-ale
(or VS Code)You'll need node
, typescript
(tsserver
), and jshint
:
curl https://webinstall.dev/node@16 | bash
export PATH="${HOME}/.local/opt/node/bin:${PATH}"
npm install -g typescript
npm install -g jshint
If you're using VS Code, type linting is built-in. You don't need any
additional plugins, just configure tsconfig.json
as mentioned below.
If you're using vim
you'll also want
vim-ale
(and probably the full set of
vim-essentials
), and update
~/.vimrc
to use typescript
(and jshint
, if desired) as the linter.
curl https://webinstall.dev/vim-ale | bash
" also use tserver for linting JavaScript
let g:ale_linters = {
\ 'javascript': ['tsserver', 'jshint']
\}
Alternatively, you can let vim
load per-project config .vimrc
:
set exrc
set secure
For other options see .vimrc.
Clone the repo:
git clone https://github.com/BeyondCodeBootcamp/jsdoc-typescript-starter
Enter and install the dependencies (mostly type definitions):
pushd ./jsdoc-typescript-starter/
npm ci
Run tsc
for a quick sanity check: \
(you should see a handful of errors scroll by)
tsc
With all that working, now open server.js
, save (to kick off the linter), and
be amazed!
vim server.js
Note: tsserver
can take 10 - 30 seconds to boot on a VPS, and may not start
until after your first save (:w
) in vim
.
A typical project will look something like this:
.
├── server.js
├── lib/
│ ├── **/*.js
│ └── **/*.d.ts
├── node_modules/
│ ├── @types/ ("Definitely Typed" typings)
│ └── <whatever>/index.d.ts (may require "default imports")
├── tsconfig.json (JSON5)
├── typings/
│ └── express/
│ └── index.d.ts (TypeScript definitions)
└── types.js (global JSDOC)
We could break this down into 4 key components, which must be referenced in your
tsconfig.json
:
.
├── server.js
└── lib/
├── **/*.js
└── **/*.d.ts
.
└── types.js
npm install --save axios
.
└── node_modules/
└── axios/
└── index.d.ts (may be compiled from TS)
Note: for modules that ship with types you may need to change how you require them to use the "default exports", (otherwise you may get really weird type errors about missing methods, etc):
- let axios = require('axios');
+ let axios = require('axios').default;
(there are also some special cases, see below for examples)
npm install --save-dev @types/express
.
└── node_modules/
└── @types/
└── express/
└── index.d.ts (community-sourced type definitions)
.
└── typings/
└── express/
└── index.d.ts (TypeScript definitions)
Note: the ./typings
folder has three widely accepted naming conventions:
named ./@types
, ./types
.
These must be properly enumerated in tsconfig.json
:
include
- this section must enumerate your local types and source code:
{
"...": "",
"include": ["./types.js", "server.js", "lib/**/*.js"]
}
compilerOptions.typeRoots
should specify your local overrides and
community type definitions.
{
"compilerOptions": {
"typeRoots": ["./typings", "./node_modules/@types"]
},
"...": ""
}
compilerOptions
must be changed from the default setting in order to make
tsserver
behaver as a linter for node rather than as a compiler for the
browser TypeScript:
{
"compilerOptions": {
"target": "ESNEXT",
"module": "commonjs",
// "lib": [], // Leave this empty. All libs in 'target' will be loaded.
"allowJs": true, // read js files
"checkJs": true, // lint js files
"noEmit": true, // don't transpile
"alwaysStrict": true, // assume "use strict"; whether or not its present
"moduleResolution": "node", // expect node_modules resolution
"esModuleInterop": true, // allow commonjs-style require
"preserveSymlinks": false, // will work with basetag
// I don't understand these well enough to tell you how to use them,
// so I recommend that you don't (they may effect includes, excludes,
// and typeRoots in expected/unintended ways).
// "baseUrl": "./",
// "paths": {},
// "rootDirs": [],
"...": ""
},
"...": ""
}
compilerOptions.lib
commented out or empty. \
(otherwise it will override target
)compilerOptions.noImplicitAny
- this will strictly warn about all (untyped)
JavaScript. You probably won't this off at first on existing projects - so
that you only lint types that you've added and care about - and then turn it
on after you've got the low hanging fruit.
{
"compilerOptions": {
"noImplicitAny": true,
"...": ""
},
"...": ""
}
If this is a new project, it's fine to turn on right away.
You may need to switch some require
s to use the "default import", for example:
// Example: axios is this way
- let axios = require('axios');
+ let axios = require('axios').default;
// Example: some modules are typed incorrectly and require some coaxing
- let axiosRetry = require('axios-retry');
+ //@ts-ignore
+ require('axios-retry').default = require('axios-retry');
+ let axiosRetry = require('axios-retry').default;
- let Papa = require('papaparse');
+ //@ts-ignore
+ require('papaparse').default = require('papaparse');
+ let Papa = require('papaparse').default;
See https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/55420 to learn more.
See ./types.js.
How to define a type and cast, and object as that type:
/**
* @typedef Thing
* @property {string} id
* @property {number} index
* @property {Date} expires
* @property {function} callback
*/
/**@type Thing*/
let thing = {
id: 'id',
index: 1,
expires: new Date(),
callback: function () {}
}
How to define a function
/**
* Does some stuff
* @param {string} id
* @returns {Promise<Thing>}
*/
async function doStuff(id) {
// do stuff
// ...
return await fetchthing;
}
How to define a hashmap / dictionary / plain object:
/**
* @typedef Thing
* ...
* @property {Record<string, any>} stuff
*/
How to define an optional property, multiple types, and union type:
/**
* @typedef Thing
* ...
* @property {string} [middle_name]
* @property {Array<Thing> | null} friends
*/
/**
* @typedef Foo
* @property {string} foo
*/
/**
* @typedef Bar
* @property {string} bar
*/
/** @type {Foo & Bar} */
var foobar = { foo: "foo", bar: "bar" };
/** @typedef {Foo & Bar} FooBar */
/** @type {FooBar} */
var foobar = { foo: "foo", bar: "bar" };
If you wanted to start a brand-new project from scratch, these are the steps you would take:
npm install -g typescript
tsc --init
npm install --save-dev @types/node
npm install --save-dev @types/express
Here's the difference between the default tsconfig.json
and the settings that
work for this project:
diff --git a/tsconfig.json b/tsconfig.json
index 35fc786..979a70d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,11 +2,11 @@
"compilerOptions": {
// "incremental": true,
- "target": "es5",
+ "target": "ESNEXT",
"module": "commonjs",
// "lib": [],
- // "allowJs": true,
- // "checkJs": true,
+ "allowJs": true,
+ "checkJs": true,
// "jsx": "preserve",
// "declaration": true,
// "declarationMap": true,
@@ -17,13 +17,13 @@
// "composite": true,
// "tsBuildInfoFile": "./",
// "removeComments": true,
- // "noEmit": true,
+ "noEmit": true,
// "importHelpers": true,
// "downlevelIteration": true,
// "isolatedModules": true,
"strict": true,
- // "noImplicitAny": true,
+ "noImplicitAny": false,
// "strictNullChecks": true,
// "strictFunctionTypes": true,
// "strictBindCallApply": true,
@@ -43,11 +43,11 @@
- // "moduleResolution": "node",
+ "moduleResolution": "node",
// "baseUrl": "./",
// "paths": {},
// "rootDirs": [],
- // "typeRoots": [],
+ "typeRoots": ["./typings", "node_modules/@types"],
// "types": [],
// "allowSyntheticDefaultImports": true,
"esModuleInterop": true,
- // "preserveSymlinks": true,
+ "preserveSymlinks": false,
// "allowUmdGlobalAccess": true,
// "sourceRoot": "",
@@ -60,5 +60,7 @@
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
- }
+ },
+ "include": ["types.js", "server.js", "lib/**/*.js"],
+ "exclude": ["node_modules"]
}
Again, the #1 thing is to make sure that include
and typeRoots
matches
how your project is set up. For this project it looks like this:
{
"compilerOptions": {
// ...
"typeRoots": ["./typings", "node_modules/@types"],
},
"include": ["types.js", "server.js", "lib/**/*.js"],
// ...
}
If you decide to follow a different convention, name your things accordingly. This is also valid, if it matches your project:
{
"compilerOptions": {
// ...
"typeRoots": ["./@types", "node_modules/@types"],
},
"include": ["**/*.js"],
// ...
}
tsc
can show you the same errors that you'll see in VS Code or vim, but in all
files across your project at once.
To check that out, run tsc
from the directory where tsconfig.json
exists.
tsc
If you don't get error output, then ake user that include
is set properly to
include all your code and JSDoc types, and that typeRoots
is set to include
your type overrides.
VS Code has TypeScript built-in, no configuration is necessary aside from the
tsconfig.json
.
Assuming that you're using vim-ale
, the main
option you need to modify is the list of linters.
For example, you must have tsserver
, and you may also want jshint
:
let g:ale_linters = {
\ 'javascript': ['tsserver', 'jshint']
\}
For other options see .vimrc