Jack Franklin

Getting started with TypeScript and React

I've recently been getting into TypeScript following a lot of positive blogs about it from Tom Dale and others. Today I'll show you how I've set up a TypeScript project from scratch that uses React, and Webpack for managing the build process. I'll also discuss my initial impressions of TypeScript and in particular working with TypeScript and ReactJS.

I won't be going into detail on the specifics of TypeScript's syntax, but you can read either the TypeScript handbook or the free book TypeScript Deep Dive which will also give you a great introduction to the language.

Update: If you'd like to read this post in German, you can do so thanks to the awesome folks at Reactx.de.

Installing TypeScript and configuring it

The first thing to do was install TypeScript locally into my node_modules directory, which I did using Yarn, first using yarn init to create a new project:

yarn init
yarn add typescript

When you install TypeScript you get the tsc command line tool which can compile TypeScript but also create a starting tsconfig.json for you to edit. You can get this by running tsc --init - if you've installed TypeScript locally you'll need to run ./node_modules/.bin/tsc --init.

Note: I have the ./node_modules/.bin directory on my $PATH, which you can find in my dotfiles. This is slightly risky, as I could accidentally run any executable that's in that directory, but I'm willing to take that risk because I know what's installed locally and it saves a lot of typing!

tsc --init generates a tsconfig.json which is where all the config for TypeScript's compiler lives. There's a few changes I've made to the default config, and the one I'm using is below:

{
"compilerOptions": {
"module": "es6", // use ES2015 modules
"target": "es6", // compile to ES2015 (Babel will do the rest)
"allowSyntheticDefaultImports": true, // see below
"baseUrl": "src", // enables you to import relative to this folder
"sourceMap": true, // make TypeScript generate sourcemaps
"outDir": "ts-build", // output directory to build to (irrelevant because we use Webpack most of the time)
"jsx": "preserve", // enable JSX mode, but "preserve" tells TypeScript to not transform it (we'll use Babel)
"strict": true,
},
"exclude": [
"node_modules" // don't run on any code in the node_modules directory
]
}

allowSyntheticDefaultImports

This rule allows you to use ES2015 style default imports even when the code you're importing doesn't have an ES2015 default export.

This happens when you import, for example, React, whose code is not written in ES2015 (the source code is, but React ships a built version). This means that it technically doesn't have an ES2015 default export, so TypeScript will tell you so when you import it. However, build tools like Webpack are able to import the right thing, so I turn this option on because I prefer import React from 'react' over import * as React from 'react'.

strict: true

TypeScript version 2.3 introduced a new config option, strict. When turned on this configures TypeScript's compiler to be as strict as possible - this might not be what you want if you're porting some JS to TS, but for new projects it makes sense to be as strict as possible out of the box. This turns on a few different settings, the most notable of which are noImplicitAny and strictNullChecks:

noImplicitAny

Often when you want to add TypeScript to an existing project TypeScript makes it easy by not throwing an error when you don't declare the types of variables. However, when I'm creating a new TypeScript project from scratch I'd like the compiler to be as strict as possible.

One of the things TypeScript does by default is implicitly add the any type to variables. any is effectively an escape hatch in TypeScript to say "don't type-check this, it can be any value". That's useful when you're porting JavaScript, but it's better to be strict when you can. With this setting set to true, you can't miss any declarations. For example, this code will error when noImplicitAny is set to true:

function log(thing) {
  console.log('thing', thing)
}

You can read more about this in the TypeScript Deep Dive.

strictNullChecks

This is another option that makes TypeScript's compiler stricter. The TypeScript Deep Dive book has a great section on this option. With this option on, TypeScript will spot more occasions where you're referencing a value that might be undefined, it will error at you. For example:

person.age.increment();

With strictNullChecks, if TypeScript thinks that person or person.age might be undefined, it will error and make sure you deal with it. This prevents runtime errors so it seems like a pretty good option to enable from the get go.

Setting up Webpack, Babel and TypeScript

I'm a big Webpack fan; I enjoy the ecosystem of plugins available, I like the developer workflow and it's good at managing complex applications and their build pipeline. Therefore, even though we could just use TypeScript's compiler, I'd still like to add Webpack in. We'll also need Babel because the TypeScript compiler is going to output ES2015 + React for us, so we'll get Babel to do the work. Let's install Webpack, Babel and the relevant presets, along with ts-loader, the Webpack plugin for TypeScript.

There is also awesome-typescript-loader, but I found ts-loader first and so far it's been great. I would love to hear from anyone who uses the awesome-typescript-loader, and how it compares.

yarn add webpack babel-core babel-loader babel-preset-es2015 babel-preset-react ts-loader webpack-dev-server

At this point I have to thank Tom Duncalf, whose blog post on TypeScript 1.9 + React was a brilliant starting point for me and I highly recommend it.

There's nothing too surprising in the Webpack config, but I've left some comments in the code to explain it:

const webpack = require('webpack');
const path = require('path');

module.exports = {
// put sourcemaps inline
devtool: 'eval',

// entry point of our application, within the `src` directory (which we add to resolve.modules below):
entry: ['index.tsx'],

// configure the output directory and publicPath for the devServer
output: {
filename: 'app.js',
publicPath: 'dist',
path: path.resolve('dist'),
},

// configure the dev server to run
devServer: {
port: 3000,
historyApiFallback: true,
inline: true,
},

// tell Webpack to load TypeScript files
resolve: {
// Look for modules in .ts(x) files first, then .js
extensions: ['.ts', '.tsx', '.js'],

// add 'src' to the modules, so that when you import files you can do so with 'src' as the relative route
modules: ['src', 'node_modules'],
},

module: {
loaders: [
// .ts(x) files should first pass through the Typescript loader, and then through babel
{
test: /\.tsx?$/,
loaders: ['babel-loader', 'ts-loader'],
include: path.resolve('src'),
},
],
},
};

We configure the loaders so that any .ts(x) file is first passed through ts-loader. This compiles it with TypeScript using the settings in our tsconfig.json - and emits ES2015. We then use Babel to convert that down to ES5. To do this I create a .babelrc that contains the presets that we need:

{
"presets": ["es2015", "react"]
}

And with that, we're now ready to write our TypeScript application.

Writing a TypeScript React component

Now we are ready to create src/index.tsx, which will be our application's entry point. For now we can create a dummy component and render it to check it's all working.

import React from 'react';
import ReactDOM from 'react-dom';

const App = () => {
return (
<div>
<p>Hello world!</p>
</div>
);
};

ReactDOM.render(<App />, document.getElementById('app'));

If you run Webpack now against this code you'll see some errors:

ERROR in ./src/index.tsx
(1,19): error TS2307: Cannot find module 'react'.

ERROR in ./src/index.tsx
(2,22): error TS2307: Cannot find module 'react-dom'.

This happens because TypeScript is trying to figure out the type of React, and what it exports, and is trying to do the same for React DOM. React isn't authored in TypeScript so it doesn't contain that information, but thankfully for this situation the community has created DefinitelyTyped, a large repository of types for modules.

The installation mechanism changed recently; all the types are published under the npm @types scope, so to get the types for React and ReactDOM we run:

yarn add @types/react
yarn add @types/react-dom

And with that the errors go away. Whenever you install a dependency you can always try installing the @types package too, or if you want to see if it has types available, you can use the TypeSearch website to do so.

Running the app locally

To run the app locally we just run the webpack-dev-server command. I set up a script, start, that will do just that:

"scripts": {
"start": "webpack-dev-server"
}

The dev server will find the webpack.config.json file and use it to build our application.

If you run yarn start you will see the output from the server, including the ts-loader output that confirms it's all working.

$ webpack-dev-server
Project is running at http://localhost:3000/
webpack output is served from /dist
404s will fallback to /index.html
ts-loader: Using typescript@2.3.0 and /Users/jackfranklin/git/interactive-react-introduction/tsconfig.json
Version: webpack 2.4.1
Time: 6077ms
 Asset     Size  Chunks                    Chunk Names
app.js  1.14 MB       0  [emitted]  [big]  main
webpack: Compiled successfully.

To view it locally I just create an index.html file that loads our compiled code:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Typescript App</title>
</head>
<body>
<div id="app"></div>
<script src="dist/app.js"></script>
</body>
</html>

You should see Hello world! on port 3000, and we have TypeScript working!

Typing a module

For a project I was working on I wanted to use the React Ace module to include a code editor in my project. However, the module doesn't provide types for it, and there is no @types/react-ace either. In this case, we have to add the types to our application so TypeScript knows how to type it. Whilst this can seem annoying, the benefits of having TypeScript at least know a little about all your third party dependencies will save you debugging time.

To define a file that has just types in, you suffix it .d.ts (the 'd' is for 'declaration') and you can read more about them on the TypeScript docs. TypeScript will automatically find these files in your project, you don't need to explicitly import them.

I created the file react-ace.d.ts, and added the following code that creates the module and defines its default export as a React component:

declare module 'react-ace' {
interface ReactAceProps {
mode: string
theme: string
name: string
editorProps?: {}
showPrintMargin?: boolean
minLines?: number
maxLines?: number
wrapEnabled?: boolean
value: string
highlightActiveLine?: boolean
width?: string
fontSize?: number
}

const ReactAce: React.ComponentClass<ReactAceProps>
export = ReactAce
}

I first create a TypeScript interface for the properties that the component takes, and then the line export = ReactAce declares that the component is the object exported by the module. By typing the properties, TypeScript will tell me if I typo a property or forget to pass one, which is really valuable.

Testing

Finally, I also wanted to have a good testing set up with TypeScript. I'm a huge fan of Facebook's Jest, and did some googling to find out if I could run it with TypeScript. Turns out it's very possible, and there's even the ts-jest package available which does all the heavy lifting. In addition, there is a @types/jest package so you can have all your tests type-checked too.

Huge thanks to RJ Zaworski, whose post on TypeScript and Jest got me started on this topic. Once you install ts-jest, you just have to configure Jest, which is done in the package.json, with some settings:

"jest": {
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"transform": {
"\\.(ts|tsx)$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
"testRegex": "/*.spec.(ts|tsx|js)$"
},

The first setting tells Jest to look for .ts and .tsx files. The transform object tells Jest to run any TypeScript files through the ts-jest preprocessor, which runs them through the TypeScript compiler and produces JavaScript that Jest can consume. Finally, I updated the testRegex setting to look for any *.spec.ts(x) files, which is my preferred naming convention for tests.

With this, I can just run jest and have everything work as expected.

Linting with TSLint

Although TypeScript gives you a lot of checks on your code, I still wanted a linter to enforce some code style and quality checks. Much like ESLint to JavaScript, TSLint is the best option for checking TypeScript files. It works in the same way as ESLint - with a set of rules that you enable or disable, and there's also a TSLint-React package to add React specific rules.

You can configure TSLint via a tslint.json file and mine is below. I use both the tslint:latest and tslint-react presets, which enables a bunch of rules. I disagree with some of the defaults though so I override them - you might choose to do differently - this is up to you!

{
"defaultSeverity": "error",
"extends": ["tslint:latest", "tslint-react"],
"jsRules": {},
"rules": {
// use single quotes, but enforce double quotes in JSX
"quotemark": [true, "single", "jsx-double"],
// I prefer no semi colons :)
"semicolon": [true, "never"],
// This rule makes each Interface be prefixed with 'I' which I don't like
"interface-name": [true, "never-prefix"],
// This rule enforces objects to always have keys in alphabetical order
"object-literal-sort-keys": false
},
"rulesDirectory": []
}

I can then run tslint --project tsconfig.json to lint my project.

Conclusion

In summary, I've found TypeScript to be a joy to work with so far. I'll definitely be blogging more about the specifics of the language and how I'm using it, but in terms of setting up a build process, configuring all the tools and getting started with types, it's been a real joy. I'd highly recommend giving it a go if you're looking for a bit more structure in your JS applications and want a strong compiler to help you avoid mistakes and spend less time debugging.

If you'd like to browse the code or get started from what I created in this post, I pushed an example repo to GitHub that you can use as a starting point. Feel free to raise an issue on that repo if you have any questions about it.