TypeScript Starter for a Better Developer Experience in 2021
TypeScript is an evolving type-safe superset of JavaScript, that offers modern language concepts and constructs. Combined with NodeJS is a favored option to write scalable applications.
A well-configured TypeScript installation can improve your Development Experience, accelerating your development workflow and reducing your cognitive load.
Here, I show how I configure TypeScript, helping me work better and faster. I explain my decisions and describe how to implement them. You will understand my approach and have a 2021 starter TypeScript installation to use in your projects.
Modern Applications and TypeScript
Modern Software Applications have the following features:
- are available on the internet,
- are accessed in multiples ways: browsers, smartphones, smart machines,
- are scalable, able to support changing and sometimes exponential demand,
- are mostly i/o intensive,
- are constantly evolving its functionality.
My preferred approach to enable those features is to:
- use Agile Methodologies,
- adopt a Microservices Architecture,
- deploy to public Cloud Providers (AWS, Azure, Google, etc).
TypeScript is an evolving open-sourced type-safe superset of the JavaScript language, backed by Microsoft, with the following characteristics:
- modern language concepts and constructs;
- static type checking at compiler time, preventing most of the runtime errors;
- compiles to plain JavaScript, so it runs almost anywhere;
- an ecosystem that enables a great developer experience;
- combined with NodeJS is a performant option for i/o intensive applications, like most of the modern web applications;
- all public cloud providers offer plenty of options to deploy NodeJS applications.
I write most of my software applications with TypeScript, using Test-Driven Development (TDD) and building Incremental Evolutions of the software.
Installation Steps
Step by step for the installation:
- Installation Structure.
- TypeScript Configuration: transpile TypeScript to JavaScript.
- Test Automation.
- Static Code Analysis.
- Formatting Automation.
- Version Control.
1) Installation Structure
We should apply separation of concerns in the structure of our project. Auxiliary files, related to installation and infrastructure should be kept separate from the files with our code.
In the root folder we should put all files that build up the installation and basic instructions:
- all configuration files;
- a README file describing the purpose of the application, how to install, run and test it.
The source code for the application should be located in a dedicated folder; I use the "src" folder. It is convenient for our application to have a CLI to call it. This CLI gets environment parameters and calls our main function. So our main function is isolated from the external environment, it receives parameters and returns a result. This allows for easier testing and cross-target building, if necessary.
In this minimal installation, the source folder ("src") will have 3 files:
- cli.ts - the CLI entry point for the application,
- main.ts - the main function that receives parameters and returns a final result,
- main.spec.ts - the test specification for the main function.
This structure can be built with the following commands:
# Create the folder for your project
$ mkdir my-project
$ cd my-project
# Create source folder and files
$ mkdir src
$ touch src/main.ts src/main.spec.ts src/cli.ts
# Create a package.json
$ yarn init -y
# Create the typescript configuration file
# (tsconfig.json)
$ tsc --init
# Create folder for the generated javascript
$ mkdir build
2) TypeScript Configuration
Edit the tsconfig.json file and define where to transpile TypeScript to JavaScript. Add these lines before "compilerOptions" settings:
"include": ["src"],
"exclude": ["node_modules", "build"],
Uncomment and define these keys under "compilerOptions":
/* Generates corresponding '.map' file. */
"sourceMap": true,
/* Redirect output structure to the directory. */
"outDir": "./build",
Keep the keys:
/* Specify ECMAScript target version */
"target": "es5",
/* Specify module code generation */
"module": "commonjs",
/* Enable all strict type-checking options. */
"strict": true,
/* Enables emit interoperability between
CommonJS and ES Modules via creation
of namespace objects for all imports.
Implies 'allowSyntheticDefaultImports'. */
"esModuleInterop": true,
/* Disallow inconsistently-cased references
to the same file. */
"forceConsistentCasingInFileNames": true
Installing TypeScript transpiler, NodeJS types, and ts-node to run TypeScript code files directly on NodeJS:
# Install as development dependencies
$ yarn add -D typescript @types/node ts-node
Edit "src/main.ts" and put this initial code:
export const main = (): string => {
return "Hello World!"
}
Edit "src/cli.ts" and put this initial code:
import { main } from './main'
console.log(main())
To run the CLI entry point and to build a javascript version of the application, edit the package.json file and just after the flag setting "license": "MIT", define the "scripts" flag setting with:
"scripts": {
"build": "tsc",
"cli": "ts-node src/cli.ts"
},
To run the application using the CLI entry point execute:
# Run the cli
$ yarn cli
It should produce the following on the console:
yarn run v1.22.5
$ ts-node src/cli.ts
Hello World!
Done in 2.00s.
To build a javascript version in the build folder:
# Build the application's JavaScript version
$ yarn build
3) Test Automation
There are good options for testing TypeScript. I prefer Jest. It's straightforward to write back-end tests with it. If necessary it can be easily combined with complementary software. For the front-end development, I use React and NextJS. Jest was built by FaceBook for testing React, so it is an easy pick.
Test automation enables Test Driven Development TDD. You should write tests specifying the desired behavior of your code, before writing the actual code. TDD requires quick and easy testing of our code as we evolve it. I use Jest with the watchAll option to continuously watch the source files and automatically rerun the tests after any change.
Install Jest to enable test automation:
# Install as development dependencies
$ yarn add -D jest ts-jest @types/jest
Create a Jest configuration file ("jest.config.json") in the root folder with this content:
{
"testEnvironment": "node",
"preset": "ts-jest",
"testMatch": ["<rootDir>/src/**/*.spec.ts"],
"watchPathIgnorePatterns": [],
"collectCoverageFrom": [
"src/**/*.{ts,js,jsx}", "!src/cli.ts"],
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
},
"coverageReporters": ["lcov", "text"],
"moduleDirectories": ["node_modules","src"]
}
This configuration file defines:
- that a test file has ".spec." in its name
- that the source code is in the "src" folder
- the test coverage parameters
To facilitate the running of tests, add to the package.json file, in the "script" flag "setting":
"test": "jest --config ./jest.config.json --watchAll",
"test:coverage": "jest --config ./jest.config.json
--coverage || open coverage/lcov-report/index.js",
All the commands for "test:coverage" should be in just one line.
Let's define the "test" file. Edit "src/main.spec.ts" and add this content:
import { main } from './main'
describe('Testing Application', () => {
it('should return Hello World!', () => {
expect(main()).toEqual('Hello World!')
})
})
Now we can develop the application fully testing it, testing a specific component, and check the test coverage. Here are the commands:
# Watch the source and execute all tests
$ yarn test
# Watch the component source and execute its tests
$ yarn test component
# Execute all tests and calculate the test coverage
$ yarn test:coverage
4) Static Code Analysis
Code quality ensures that our code won’t fail on avoidable errors. Lint tools improve code quality by doing static analysis: checking code for readability, maintainability, and functionality errors, and also may modify the code, improving it. They help prevent that our code break. Accidentally misspelling a variable, letting case/switch statements fall through, or other silly mistakes that will potentially break your application.
There are many preconfigured lint tools for TypeScrit. Most of them are ESLint configurations or plugins. Some of the most used are unicorn and airbnb. Here is a list of TypeScript lints.
I use plain ESLint, that is a TypeSrcipt Lint tool that works with rules.
To install ESLint:
$ yarn add eslint @typescript-eslint/eslint-plugin \
@typescript-eslint/parser --dev
Create a configuration file ".eslintrc" in the root folder with this content:
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "error"
},
"parserOptions": {
"ecmaVersion": 2021
},
"extends": [
"plugin:@typescript-eslint/recommended"],
"overrides": [
{
"files": ["*.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off"
}
}
]
}
5) Formating Automation
The readability is improved if we adopt a unique format for the code. Prettier formats the code to a common standard. I use ESLint combined with Prettier for linting and formating.
To install prettier combined with ESLint:
$ yarn add prettier eslint-config-prettier \
eslint-plugin-prettier --dev
Create a configuration file ".prettierrc" in the root folder:
$ touch .prettierrc
To configure "ESLint" to work in conjunction with "Prettier", modify ".eslintrc":
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "error"
},
"parserOptions": {
"ecmaVersion": 2021
},
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"overrides": [
{
"files": ["*.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off"
}
}
]
}
6) Version Control
Incremental Evolution is based on implementing, testing, and saving small advances in the work, as small milestones towards the desired outcome. The walking skeleton model is a pattern for that. This allows a safe evolution and an overall faster development. It is done with TDD and the support for versioning in a repository. I use git for versioning and github.com to put my repositories.
This command (executed at the root folder) creates a repository:
$ git init
The repository should keep versions of just the files necessary to rebuild our project. The ".gitignore" at the root folder file defines files that need not be in our repository and should be ignored. Here is the Microsoft recommendation of it. I just change the "dist" folder for "build".
Bonus: Simplify Import Statements
Bigger projects mean more source folders and files. Possibly other collaborators. Presence of "unfamiliar" code. All of these increase the cognitive load to work. To work effectively in bigger Projects, it is necessary to reduce the cognitive load, making it easier to understand the code.
As our projects grow and the code is distributed among different folders, forming a tree, each folder corresponding to a module. To facilitate the importing of references from modules, in a way that encapsulates the implementation and location of the modules, we can use two patterns:
- export from the modules - use index.ts files inside each folder, that import all the references from the files in this folder, and export from there. External code will be able to import those references specifying as the location only the folder name, no need to know the specific file names.
- absolute module names - use abbreviated absolute names for these modules, independently of the relative positioning of the exporting and importing modules.
These patterns reduce the cognitive load and the coupling between the modules.
To enable absolute module names I use module-alias. To install it:
$ yarn add -D module-alias
Edit these setting flags into "tsconfig.json", inside "compilerOptions":
/* Base directory to resolve non-absolute module names */
"baseUrl": "./src",
/* Entries which re-map imports relative to the 'baseUrl' */
"paths": {
"@/*": ["*"]
}
To enable Jest to understand the alias add this stting flag to "jest.config.json", just after "moduleDirectories":
"moduleNameMapper": {
"@/(.*)$": "<rootDir>/src/$1"
}
Insert this line at the very main file of your app, before any code
import 'module-alias/register'
Now to import just precede the folder name with '@'
# instead of
import * from '../../../../some/very/deep/module'
# you can use
import * from '@module'
Now we have a TypeScript Starter configuration that helps us to be better developers having a better developer experience.
The repository with this installation up to here is hosted here.