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!
- Key Benefits
- Prerequisites
- Starter / Demo
- Key Components
- Key Configuration
- JSDoc Cheat Sheet
- Manual "From Scratch"
- Vim Configuration
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:
-
requireing a file that doesn't exist - access an
undefinedproperty - a property is only defined in an
if { ... }condition - a method on a value from a function that sometimes returns
null - IntelliSense / AutoCompletion
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.
gitnodetypescript(andjshint)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 jshintIf 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 secureFor other options see .vimrc.
Clone the repo:
git clone https://github.com/BeyondCodeBootcamp/jsdoc-typescript-starterEnter and install the dependencies (mostly type definitions):
pushd ./jsdoc-typescript-starter/ npm ciRun tsc for a quick sanity check:
(you should see a handful of errors scroll by)
tscWith all that working, now open server.js, save (to kick off the linter), and be amazed!
vim server.jsNote: 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:
- Source code (JavaScript + JSDoc)
. ├── server.js └── lib/ ├── **/*.js └── **/*.d.ts
- Local typings (JSDoc)
. └── types.js
- Typed Modules:
(modules that ship with types)npm install --save axios
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):. └── node_modules/ └── axios/ └── index.d.ts (may be compiled from TS)
(there are also some special cases, see below for examples)- let axios = require('axios'); + let axios = require('axios').default;
- "Definitely Typed" definitions:
(community-sourced typings for popular packages)npm install --save-dev @types/express
. └── node_modules/ └── @types/ └── express/ └── index.d.ts (community-sourced type definitions)
- Type overrides (Type Definitions) Note: the
. └── typings/ └── express/ └── index.d.ts (TypeScript definitions)
./typingsfolder 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.typeRootsshould specify your local overrides and community type definitions.{ "compilerOptions": { "typeRoots": ["./typings", "./node_modules/@types"] }, "...": "" }
-
compilerOptionsmust be changed from the default setting in order to maketsserverbehaver 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": [], "...": "" }, "...": "" }
- Leave
compilerOptions.libcommented out or empty.
(otherwise it will overridetarget)
- Leave
-
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
requires 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 DefinitelyTyped/DefinitelyTyped#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/expressHere'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.
tscIf 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

