You don't want to read through all those steps? Dive in straight into coding, just clone/download/fork this repository with the resulting base project of this post 😀.
Whenever learning a new language after configuring a proper development environment, set up a project from scratch is the next step on my list.
To understand how to have everything in the right place to start coding is essential. Of course, after the first few times, you will probably automate these steps or would rely on some boilerplate project.
The idea of this post is to walk through the very first steps necessary to have a project correctly set up for Typescript, and by correctly, I mean having these things in place:
This structure could be used for front-end development, probably with some tweaks here and there. But for my purpose and also for the post, it is oriented for backend development.
Also, everything here is aimed towards *nix environments, either be some Linux flavor or MacOs, with NodeJS and Git installed.
Define the folder name that will house our project. In this case, let's call it ts-project.
mkdir -p ts-project/{src,tests/unit/src}
The above command will generate this structure:
ts-project ├── src └── tests └── unit └── src
Hop into the project's folder.
cd ts-project
Initiate an empty Git Repository:
git init
Add a .gitignore file at the root of the project with the following content:
node_modules/
dist/
Which will tell Git to not track the changes on those folders.
Initiate an NPM project. The -y tells NPM to accept all the default settings:
npm init -y
Install Typescript:
npm install --save-dev typescript
Do not ignore the --save-dev flag. It tells NPM to add the Typescript package to the dev dependency list on our newly added package.json.
Initiate Typescript by issuing:
npx tsc --init
This line deserves a word or two. Alongside NPM, it is installed another tool called NPX. NPX is a tool to execute binaries without having them installed globally. It will look for the executable first at the environment variable $PATH, then in the local project for the requested command, in this case, tsc.
The tsc portion of the command refers to the Typescript dependency. When executed, the above command should display something like this as a result:
message TS6071: Successfully created a tsconfig.json file.
It creates a configuration file called tsconfig.json with parameters necessary for Typescript to work properly.
By default, all possible configuration keys are present, but most of them, will be commented out. After cleaning up the unnecessary commented lines you will be left with something like this:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
For a detailed description of what each of these fields means, please check the official documentation right here.
Let's tweak this a little bit. Add two new keys to the compiler options
"outDir": "dist",
"sourceMap": true
And at the root we add:
"include": [ "./src/**/*" ]
Which tells the compiler to take everything from the source (src) folder.
The end result is something like this:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist"
},
"include": [ "./src/**/*" ]
}
For unit tests, I have been using Jest for quite some time now, no complaints.
Very straight forward and simple testing framework.
To install all the packages necessary run:
npm install --save-dev \
jest \
babel-jest \
@babel/core \
@babel/preset-env \
@babel/preset-typescript \
@types/jest
Then add a babe.config.js at the root of the project with the content:
module.exports = {
presets: [
[ '@babel/preset-env', { targets: { node: 'current' } } ],
'@babel/preset-typescript',
]
};
And our testing setup is done.
This is a crucial step to ensure, among other things consistency. I am have been working with the Airbnb style guide for Javascript for almost two years, and love it. Helps to fix smaller mistakes literally pointing out to you.
To be able to use the same ruleset on Typescript, we are going to use a package called eslint-config-airbnb-typescript, which is a drop-in replacement for the normal eslint-config-airbnb but with all the Typescript goodness.
To install, run the following:
npm install --save-dev \
eslint \
eslint-config-airbnb-typescript \
eslint-plugin-import@^2.22.0 \
@typescript-eslint/eslint-plugin@^4.4.1
And add .eslintrc.js to the project root with the content:
module.exports = {
extends: ['airbnb-typescript/base'],
parserOptions: {
project: './tsconfig.json',
},
};
In case you went through my last post "Setting up Neovim for typescript development" you will notice that this style guide uses Eslint, and we configured the only tsserver. To add Coc support for Eslint run:
:CocInstall coc-eslint
Check out its documentation to learn more about the extension.
Let's leverage the NPM scripts system to facilitate interacting with the tooling we just set up.
This seems like a trivial, maybe unnecessary step, but having the tooling abstracted by the scripts can help to decouple it from other parts like some editor shortcuts or CI/CD pipelines. So in case, you decide to change your testing library or build process, we can simply change it in one place.
Add this piece of code at the root of the package.json:
"scripts": {
"test": "jest",
"lint": "eslint",
"compile": "tsc"
}
These are pretty self-explanatory, but here are examples of how we can use these scripts. From the project root run:
# This will run the testing library Jest
npm run test
# This will run the linting
npm run lint
# This will run the compilation
npm run compile
Finishing up with some fail-safes, it can make our lifes much easier. Git hook is a neat feature from Git, it allows us to run scripts in certain key events like before applying a commit, before pushing, and many others.
In this example, we will use a package called pre-commit to run our scripts before the commits. To install it, run:
npm install --save-dev pre-commit
And then add this to package.json:
"pre-commit": [
"test",
"lint",
"compile"
],
This will make sure that every time you issue a commit command, runs all the three npm scripts. That way, we will never be able to event commit broken or invalid code.
With everything in place, let's write a "hello world" and test it. Add an index.ts to your src folder, located at the root of the project with this content:
/**
* Hello world function
*
* @param {string} name
*/
function helloWorld(name: string) {
return `Hello world, ${name}`;
}
export default helloWorld;
And add an index.spec.ts at tests/unit/src with this content;
import helloWorld from '../../../src/index';
test('Hello world works', () => {
expect(helloWorld('Foo'))
.toBe('Hello world, Foo');
});
Now, from the command line at the root of the project, run:
npm run lint && npm run test && npm run compile
Which should result in something like this:
> ts-project@1.0.0 lint /Users/username/ts-project
> eslint
> ts-project@1.0.0 test /Users/username/ts-project
> jest
PASS tests/unit/src/index.spec.ts
✓ Hello world works (2 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.898 s, estimated 1 s
Ran all test suites.
> ts-project@1.0.0 compile /Users/username/ts-project
> tsc
We've made it!
These kinds of steps are valuable to understand all the moving parts that compose a base project structure, but after a couple of times, these steps should be automated or a boilerplate that fits your needs created.
We went through all the steps to set up a solid foundation for our experiments and projects. Now the only thing left is to build something cool.