I have used C++, C# and Java for a long time, and love the short, uncluttered view of JavaScript code. Yet, I find myself struggling to refactor my own code without breaking things. It is time to introduce a little bit of help to my source. I tried Flow using source comments, yet could not see many obvious benefits. Recently I have tried TypeScript (v2.2) with Visual Studio Code editor (v1.10) and must admit that it was a very positive experience. Here is a short tutorial to get anyone started with TypeScript.
- Visual Studio Code editor
- Writing Node app in TypeScript
- TypeScript
- Types
- 3rd party types
- Ambient types
- Manual types for 3rd party libraries
- Linting
- IntelliSense and JSDoc
- Types from JSDoc
- Source maps
- Favorite features
- Common problems and solutions
- I just want to run TypeScript files in Node
- I want ts-node with auto reload
- Ignore specific error
- Ignore all TS errors in a file
- Iterable
- I need to write TypeScript tests
- Cannot find global symbol describe (Mocha, Jasmine)
- Cannot redeclare block-scoped variable
- Nested folders
- Cannot find name process
- Cannot find system module
- Promise refers to a type
- Loading JSON files
- CPU is fried
- Atom editor
- Objects with values of same type
- Provide types during destructuring
- Cannot find property "find"
- Cannot find name 'require'
- Ramda types are not working
- Read only objects and arrays
- Non-empty array
- Debugging TypeScript from VSCode
- Adding property to
global
object
- Built-in and 3rd party types
- Additional information
Visual Studio Code editor
I like Sublime text editor so much! It is fast and has thousands of plugins. When I tried Visual Studio Code (VSCode) it came with built-in IntelliSense, Git integration (showing me changed and added code lines and good color themes. It felt like using Sublime with batteries included by default.
Writing Node app in TypeScript
Let us start with an empty Node project.
1 | $ npm init --yes |
Let us place all TypeScript files into one folder src
and output JavaScript
code into dist
folder. We should also specify the configuration for the
TypeScript compiler in tsconfig.json
file
1 | $ mkdir -p src |
This configuration (that is very simple to change later)
outputs modern JavaScript, but transpiles ES6 modules into CommonJS ones.
If you want to use ES6 modules (for tree-shaking bundling for example),
you can configure a separate step and use "module": "es6"
setting.
For now, let us just get working Nodejs code.
Install TypeScript compiler (I prefer to install every tool as a local dev dependency rather than globally) and connect it to the build command
1 | $ npm i -D typescript |
Update package.json
1 | { |
Let us write TypeScript!
TypeScript
There is no magic to TypeScript. In fact, the easiest way to start is to write
plain JavaScript! Just give it file extension ts
. I will use Lodash and Ramda
to form a greeting string
1 | // src/greet.ts |
Let us use the exported function greet
from another file.
1 | // app.ts |
If you hover over greet(' world ')
function, the IntelliSense pops up and
shows the type signature greet(user: any): string
. Notice that we do not
know the input type, yet it has correctly inferred the output type -
this function returns a string.
Let us build and run the code. Add the start
command to package.json
file
1 | { |
1 | $ npm start |
Excellent, it works!
Types
What happens if we pass a number to our function greet
?
1 | console.log(greet(42)) |
It crashes, of course
1 | hello World |
We can catch this error using TypeScript compiler (and even earlier
using VSCode IntelliSense). We can provide type information,
for example we can specify that our
function greet
is expecting a string argument. Then any attempt to
call it with a non-string argument will show an error.
1 | export function greet(user: string) { |
1 | $ npm start |
The editor is showing red squiggly lines under console.log(greet(42))
showing the error if I hover it. Helpful!
3rd party types
What about the little functions we are using from Lodash and Ramda? By default, there is no IntelliSense or type information for them
1 | import {capitalize} from 'lodash' |
Luckily, type definitions for many libraries have been added under @types
scope name as part of the DefinitelyTyped project. Just bring these
types as dev dependencies
1 | $ npm i -D @types/lodash @types/ramda |
Hover over trim(...)
call in the greet.ts
and notice helpful type
definition trim(str: string): string - Removes (strips) whitespace from both ends of the string
.
Ambient types
Imagine you have a library written in "classical" CommonJS and you would like to use it from a TypeScript project. You can add ambient type definition file and list types there.
If your library has the main file "main.js", add "main.d.ts" next to it and describe the types. For example
1 | // main.js |
Add link to main.d.ts
to your package.json
, do not forget to include it
in the published package.
1 | { |
Then from your TypeScript project, require the module "my-lib".
1 | // client.ts |
You can find more examples of writing ambient type definitions in the official docs
Manual types for 3rd party libraries
If the 3rd party library does not provide any types, you can "fill" the
missing information yourself. For example, imagine you are importing my-math
module which will give you a couple of methods
1 | // node_modules/my-math/index.js |
Tell TypeScript compiler that you want to use your own type definition file
1 | // index.ts |
Write the libs.d.ts
file. It should define each module you want to describe
and the exported value. The only thing that might trip you up: you will
describe an interface of my-math
, but export a named value of that
interface.
1 | // libs.d.ts |
TypeScript compiler should be able to use these definition for the loaded
CommonJS module. A good idea is to put all your type definitions into
a file named typings.d.ts
in the root of the TypeScript code.
Linting
I love using standard to enforce consistent code style and even automatically fix small issues. For TypeScript, there is tslint and "standard" set of rules
1 | $ npm i -D tslint tslint-config-standard |
Set the rule in tslint.json
file
1 | $ cat <<EOF > tslint.json |
and run linter before each build
1 | { |
Most white space errors will be fixed automatically.
Linting using TypeScript compiler
You can also lint (without auto-fixing) TypeScript files using tsc
itself. Just specify options to avoid outputting JS
1 | { |
Example output:
1 | > tsc --pretty --noEmit cypress/**/*.ts |
We can solve the first problem (missing global it
function) by adding Mocha TypeScript definitions with npm i -D @types/mocha
. We can solve the second by installing version of Cypress that includes type definitions (should be > 1.1.3
) or by installing definitions from deprecated library npm i -D @types/cypress
.
IntelliSense and JSDoc
TypeScript IntelliSense in VSCode and Atom will automatically display JSDoc comments with the type definitions. For example, write a short one liner above the function to be displayed
1 | /** Adds two numbers or concatenates two strings */ |
Here is how VSCode and Atom shows the type and help comment for a typical function.
Types from JSDoc
You don't need to use TypeScript to add types to your JavaScript code. You can use JSDoc comments like this
1 | /** |
You must use JSDoc comment format though - simply using // ...
does not work
1 | /** @type {number} */ |
Whenever using an object you can quickly describe its properties:
1 | /** |
Syntax @property {boolean=} log
means the property log
is optional. Another way to write it is @property {boolean} [log]
.
Read TypeScript without TypeScript -- JSDoc superpowers and flip through slides TypeScript checks without TypeScript. You can also see JSDoc in action in this video.
Source maps
Imagine there is a crash inside our transpiled code. For example, if the original TypeScript code is
1 | import {capitalize} from 'lodash' |
then running the transpiled code generates a stack trace that does NOT point
at line 6 of greet.ts
. Instead it points at the transpiled JavaScript code.
1 | $ node dist/greeter.js |
We need to do 2 things to get the stack traces pointing at the original code.
- TypeScript compiler should generate source maps. Go to
tsconfig.json
and addsourceMap
setting.
1 | { |
Running the tsc
compiler now should produce extra ".map" files, and each
".js" file will have source map reference
1 | $ cat dist/greet.js |
- Add source map support to node by installing node-source-map-support module. It is a runtime dependency.
1 | $ npm install --save node-source-map-support |
- Import the
node-source-map-support
at the start of your TypeScript application
1 | import 'source-map-support/register' |
The crash should now show the original TypeScript locations.
1 | $ node dist/greeter.js |
This is still imperfect (what is Object.greet
?), but at least can be
prettified using pretty-error
module
1 | import 'source-map-support/register' |
which produces the following (plus colors)
1 | $ node dist/greeter.js |
There are other themes you can use.
Once you have a folder with original TypeScript files .ts
, and generated JavaScript files
.js
and the source map files .js.map
, the file explorer view becomes very crowded.
To hide the generated files when there is an existing TypeScript file,
open VSCode Preferences, find files.exclude
settings object and add the following
1 | { |
Favorite features
Visual Studio Code has a great feature: hover over a function reference, for
example over "greet" in console.log(greet('me'))
and select
"Peek definition" from right click menu.
The modal shows quick function definition!
Refactoring a code base is also easier with "Find references" feature
Common problems and solutions
I just want to run TypeScript files in Node
Use ts-node
I want ts-node with auto reload
Use ts-node-dev
Ignore specific error
If you are running tsc
and getting annoying errors, like "unused variable", you can ignore specific error for specific line. For example
1 | // variable "i" is unused by needed. Ignore this specific error. |
Ignore all TS errors in a file
Use the // @ts-nocheck
comment, see TS v3.7 announcement
Iterable
If TypeScript compiler cannot find Iterable
or similar things, then in the tsconfig.json
add to lib
1 | { |
If it cannot find things like window
, HTMLElement
, then add dom
to the lib
list.
I need to write TypeScript tests
I have described how to transpile TypeScript spec files in Unit test Node in 10 seconds blog post.
Ava
Another good solution is to use Ava test runner but via ava-ts. Then you can write TypeScript tests with zero configuration. Even running a single spec file is simple using npx tool.
1 | $ npx ava-ts spec/foo-spec.ts |
Mocha
If you use Mocha, you can write your spec files in TypeScript and use ts-node as a preprocessor for Mocha
1 | $ npm i -D ts-node |
1 | { |
Jest
If you want to use Jest, use it via ts-jest. Install with command
1 | $ npm install --save-dev jest ts-jest @types/jest |
The add config block to package.json
1 | { |
If your spec files all are next to the source files, for example in src
folder and all end with -spec.ts
, the configuration could be very simple
1 | { |
Note: do NOT remove "js"
from the moduleFileExtensions
list - it will break Jest loader! Now create folder __tests__
and write test file __tests__/foo-test.ts
and the tests should have type support.
Note: if the test run returns error without explanation like the one below, make sure you have tsconfig.json
(generate it with npx tsc --init
command)
1 | ● Test suite failed to run |
Wallaby.js + Jest
I have working example with Wallaby.js test runner using Jest and TypeScript in https://github.com/bahmutov/test-wallaby
Cannot find global symbol describe (Mocha, Jasmine)
When testing using Mocha we tell linter that describe
and it
are global
symbols
1 | /* global describe, it */ |
When moving this code to TypeScript, find if there are Mocha typings using
1 | $ npm info @types/mocha |
Remove the lint comment - VSCode and TypeScript compiler will understand the types now automatically.
Cannot redeclare block-scoped variable
When porting JavaScript we can get a cryptic error if multiple files declare same variables. For example if two files require same modules using same variable names we will get an error.
1 | const debug = require('debug')('my-module') |
To solve I found the solution to use import
declaration instead of const
.
You don't even need to change everything, just put a module that can be
imported.
1 | import la = require('lazy-ass') |
Another solution people suggest is tell TypeScript compiler that these are ES6 modules by exporting from the top level of the file. Just add a "dummy" export statement at the top level of the file
1 | export {} |
Nested folders
To convert multiple files in subfolders and generate same folder tree in
the output folder, use rootDir
option. For example, to convert all
TypeScript files from ts
folder and output the results into src
directory use
1 | { |
See #2772
Cannot find name process
If you get an error trying to use common Node global symbols, like process
with error
message error TS2304: Cannot find name 'process'
you should install the node typings
1 | npm install "@types/node" --save-dev |
The VSCode window might need to be reloaded in order to stop showing red squiggly lines.
Cannot find system module
1 | import fs from 'fs' |
Produces error TS2307: Cannot find module 'fs'
solution
Make sure tsconfig.json
file has "moduleResolution": "node"
as in
1 | { |
Then install Node typings npm install "@types/node" --save-dev
and change the import
line to import * as fs from 'fs'
.
Promise refers to a type
1 | new Promise((resolve, reject) => ...) |
produces an error Promise' only refers to a type, but is being used as a value here.
solution
Make sure the tsconfig.json
has ast least es6
target type
1 | { |
Loading JSON files
To load a JSON file, like we do in JavaScript const pkg = require('./package.json')
you need to define a wildecard module. Just create a typings.d.ts
file
with
1 | declare module '*.json' { |
and then import the file. When you are ready to grab a property from the loaded object, cast it as "any" or a specific interface
1 | import * as pkg from './package.json' |
See the blog post How to Import json into TypeScript
Alternatively, you can still use require
to load JSON files, but you
would need to add Node type definitions
1 | $ npm install @types/node --save-dev |
CPU is fried
Fun fact: VSCode uses a lot of CPU when rendering blinking cursor. Disable it in the preferences
1 | "editor.cursorBlinking": "solid" |
Atom editor
To add TypeScript support to Atom editor, I recommend
atom-typescript. The first time
you open a .ts
file, atom-typescript will ask if you want to install
linter plugin. Click "Yes" and let it install.
I must admit, Atom TypeScript support via plugin is not as good as in VSCode. The notifications often just showed the error, but without providing relevant help. For example, as I am typing arguments, Atom only shows that there is an error, but not the actual signature!
Objects with values of same type
To avoid losing type information when storing values in an object, you need to provide type definitions. For example if we want to have Group
that can only have Person
values we would need to declare the type of the group
object explicitly.
1 | type Person = { |
Unfortunately you cannot use type aliases as keys in type declarations. For example if we store people using social security numbers we cannot do this (yet as of TS v2.8.0).
1 | // type alias |
Provide types during destructuring
If you need to specify types during object destructuring, like the type for message
in the example below you need to declare the entire object. See Typing Destructured Object Parameters in TypeScript
1 | // how to specify type for "message"? |
Cannot find property "find"
If the TypeScript cannot find property find
on an array instance, like here
1 | [1,2,3].find(_ => _ === 3) |
Then create tsconfig.json
file and set the target
property to es6
1 | npx tsc --init |
1 | { |
Cannot find name 'require'
If you get this error
1 | error TS2304: Cannot find name 'require'. |
It means TypeScript compiler does not understand that you are working in Node environment. Just add its types
1 | npm install @types/node --save-dev |
Ramda types are not working
If you are trying to use Ramda library and the types get "lost"
- add
npm i -D @types/ramda
to bring Ramda's types - use
import {prop} from 'ramda'
instead ofconst {prop} = require('ramda')
to get correct operators
Read only objects and arrays
You can prevent accidental mutations using Readonly
and ReadonlyArray
types.
For deeper freeze, you need to use Readonly
on each property
Use ReadonlyArray
to prevent changes, even mutation methods are disabled
Non-empty array
Here is how to declare a type for an array that should never be empty
1 | type UnemptyArray<T> = [T, ...T[]] |
Note that you can still change the length of the array to make it empty at run time.
1 | strings.length = 0 |
And I could not find a way to combine readonly arrays with non-empty arrays :(
Debugging TypeScript from VSCode
If you are using VSCode to edit TypeScript code used with ts-node
, you can set break points and quickly debug the program's execution. Create .vscode/launch.json
file and define a command there
1 | { |
The above config loads node -r ts-node/register src/app.ts
and everything should just work!
Adding property to global
object
To describe additional properties your code adds to Node.js global
object describe changed properties like this
1 | declare global { |
TypeScript will merge your NodeJS.Global
instance with the built-in one.
Built-in and 3rd party types
Here are all the built-in types: Comprehensive list of built-in utility types in TypeScript
And here is a library of additional useful functions for creating own types type-fest
Additional information
A few links to why and how you should use TypeScript
Tutorials
- An Introduction to TypeScript by Daniel Gynn is good.
- Introduction to TypeScript by Kenny Song is pretty short.
- TypeScript Introduction by Todd Motto
- TypeScript Deep Dive eBook
Blog posts and videos
- All JS libraries should be authored in TypeScript
- 45 is a good example of a test runner written in TypeScript, and typescript-library-starter is a good way to start a TypeScript library (includes bundling and tree-shaking)
- Migrating from JavaScript
- How we migrated a 200K+ LOC project to TypeScript and survived to tell the story presents a good argument for switching an existing project to TypeScript.
- Video TypeScript tooling for greater productivity - Martin Probst and Alex Eagle has Angular-specific parts, but is really high quality intro to TypeScript.