Raise the Strictness Bar

TypeScript

This is likely the most significant item on your checklist, yet also the one that will consume the most time. When strictness is turned on, simple errors are caught during compilation, creating a more secure coding environment. For instance, it flags unsafe access on objects that may be null.

To switch on "strict mode" in your project, locate the tsconfig.json file and assign the compilerOptions.strict property a value of true. The strict flag aggregates several other strictness rules. Beyond this flag, additional options can be turned on to further harden the setup—such as noImplicitOverride, noPropertyAccessFromIndexSignature, noImplicitReturns, and noFallthroughCasesInSwitch. These extras come pre-enabled in a fresh Angular 13 generation.

Angular Templates

TypeScript strict mode is not the only layer; Angular's strictTemplates option serves a similar purpose but targets HTML templates. As an example, it raises a warning if you pass a parameter with an incorrect type—a string in place of a numeric value, say.

To activate strictTemplates, edit the tsconfig.json file and set the angularCompilerOptions.strictTemplates value to true.

Activate Strict Mode

The initial attempt to switch on either strict option—or both—will likely produce a set of errors when you attempt to run or build the application. These issues have to be resolved before the app will function again.

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2017",
    "module": "es2020",
    "lib": [
      "es2020",
      "dom"
    ]
  },
  "angularCompilerOptions": {
    "strictTemplates": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Check the TypeScript reference and the Angular documentation for further details.

Betterer

While fixing every error at once is the ideal situation, don't panic when the list seems overwhelming.
Fortunately, Betterer offers a way to progressively enhance the codebase's quality.

With Betterer, you're not obligated to resolve all errors immediately, allowing development to proceed without interruption.
This approach means you don't have to "lose" time before getting the application running.
Betterer lets you address issues at your own pace, one at a time, while ensuring no new issues creep in — and this can be a collaborative endeavor.

To get started with Betterer, execute the init command:

# Install Betterer and create a blank test
npx @betterer/cli init
# Install the plugins we need
npm i --save-dev @betterer/typescript @betterer/angular
Enter fullscreen mode Exit fullscreen mode

Afterward, eliminate the strict option from the tsconfig.json file (the one we added earlier) and transfer those settings into a Betterer test within the .betterer.ts file.

import { typescript } from '@betterer/typescript';

export default {
  'stricter compilation': () =>
    typescript('./tsconfig.json', {
      strict: true,
    }).include('./src/**/*.ts'),
};
Enter fullscreen mode Exit fullscreen mode

Prior to running the Betterer command, append the --strict flag to the new betterer script in the package.json file, which makes it more difficult to manipulate the test outcomes.

{
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "betterer": "betterer --strict"
  },
  "dependencies": {},
  "devDependencies": {
    "@betterer/cli": "^5.1.6",
    "@betterer/typescript": "^5.1.6",
    "@betterer/angukar": "^5.1.6"
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, you're set to execute Betterer for the initial time, which produces the output shown below.

npm run betterer

   \ | /     _         _   _
 '-.ooo.-'  | |__  ___| |_| |_ ___ _ __ ___ _ __
---ooooo--- | '_ \/ _ \ __| __/ _ \ '__/ _ \ '__|
 .-'ooo'-.  | |_)|  __/ |_| ||  __/ | |  __/ |
   / | \    |_.__/\___|\__|\__\___|_|  \___|_|

🎉 Betterer (4.743ms): 1 test done!
✅ stricter compilation: "stricter compilation" got checked for the first time! (291 issues) 🎉

1 test got checked. 🤔
1 test got checked for the first time! 🎉
Enter fullscreen mode Exit fullscreen mode

As observed, the Betterer command identifies violations specified in the test file.
In this instance, TypeScript strictness is enabled.
What remains hidden is that it saves the outcomes in a dedicated .betterer.results file.

On subsequent runs, Betterer compares the two sets of results and raises an error if the outcome has deteriorated.

🎉 Betterer (3.809ms): 1 test done!
✅ stricter compilation: "stricter compilation" got better! (0 fixed issues, 291 remaining) 😍

・ New issue in "/work/project/src/state.ts"> 2 | import {  BehaviorSubject, throwError } from 'rxjs';
・     |                            ^^^^^^^^^^ 'throwError' is declared but its value is never read.

1 test got checked. 🤔
1 test got worse. 😔
Enter fullscreen mode Exit fullscreen mode

Excellent! You're now able to spot new violations and stop them from being committed (we'll explore this further later).

Once you've genuinely made improvements, Betterer lets you finalize the modifications, and it refreshes its results file.

🎉 Betterer (6.809ms): 2 tests done!
✅ stricter compilation: "stricter compilation" got better! (49 fixed issues, 242 remaining) 😍

1 test got checked. 🤔
1 test got better! 😍
Enter fullscreen mode Exit fullscreen mode

Multiple tests can be incorporated into the .betterer.ts file, for instance, we could also add a test for the Angular strict templates option.

import { typescript } from '@betterer/typescript';
import { angular } from '@betterer/angular';

export default {
    'stricter compilation': () =>
        typescript('./tsconfig.json', {
          strict: true,
        }).include('./src/**/*.ts'),

    'stricter template compilation': () =>
        angular('./tsconfig.json', {
            strictTemplates: true
        }).include('./src/*.ts', './src/*.html'
};
Enter fullscreen mode Exit fullscreen mode

Update Angular

Another task on your checklist is confirming that the project operates on the newest Angular release.
If you discover it doesn't, consider updating Angular.
Typically, the process takes anywhere from a few minutes to about an hour.
Should you hit a snag, you have the option to halt the upgrade and document both the successes and the hurdles — that information proves invaluable for planning the update later. Additionally, if the most recent Angular version was only released a few weeks ago, consult your teammates about whether it's acceptable to upgrade, since there might be a company policy restricting version changes.

Updating Angular's dependencies is straightforward, and the official Angular Update Guide provides comprehensive guidance with a detailed, step-by-step migration path.

To determine if a dependency is outdated, execute the ng update command.
When the project isn't utilizing the latest version, you'll see output similar to what's shown below.

npx ng update

The installed local Angular CLI version is older than the latest stable version.
Installing a temporary version to perform the update.
Installing packages for tooling via npm.
Installed packages for tooling via npm.
Using package manager: 'npm'
      @angular/cdk                            11.2.13 -> 12.2.9        ng update @angular/cdk@12
      @angular/cli                            11.2.11 -> 12.2.9        ng update @angular/cli@12
      @angular/core                           11.2.12 -> 12.2.9        ng update @angular/core@12
      @ngrx/store                             11.1.1 -> 13.0.2         ng update @ngrx/store

    There might be additional packages that don't provide 'ng update' capabilities that are outdated.
    You can update the additional packages by running the update command of your package manager.
Enter fullscreen mode Exit fullscreen mode

Next, provide the desired libraries as arguments to the ng update command and allow the Angular CLI to handle the rest.

npx ng update @angular/cli@12 @angular/cdk@12 @ngrx/store
Enter fullscreen mode Exit fullscreen mode

Discover how to incorporate your own libraries into the update command in ng update: the setup

Moving to ESLint

Angular's early days leaned on TSLint for static analysis, letting developers catch common issues quickly. That all shifted around 2019–2020 when TSLint went into deprecation, and the community rallied around ESLint, ported for TypeScript via typescript-eslint.

Because TSLint shipped with new Angular scaffolds out of the box, plenty of older codebases are still wired up with it. That leaves us with one more migration chore: switching from TSLint to ESLint.

For Angular specifically, there's the angular-eslint plugin, which fills the role codelyzer played before.

The good news is the angular-eslint maintainers put real effort into an automatic migration path, making the transition mostly painless. To bring your project over to ESLint, these commands will do the trick.

npx ng add @angular-eslint/schematics
npx ng generate @angular-eslint/schematics:convert-tslint-to-eslint
Enter fullscreen mode Exit fullscreen mode

The script converts existing TSLint rules into their ESLint counterparts and tries to map any TSLint plugins you have installed to suitable ESLint equivalents. While you're at it with installation and setup, I'd suggest adding the RxJS ESLint plugin as well—and if your app uses NgRx, the NgRx ESLint Plugin is worth including too.

Linters do more than just flag mistakes; many rules come with automatic fixers, including for certain deprecated patterns and recommended practices.

For a lean project, the resulting ESLint setup tends to look like this.

{
  "root": true,
  "ignorePatterns": [
    "projects/**/*"
  ],
  "overrides": [
    {
      "files": [
        "*.ts"
      ],
      "parserOptions": {
        "project": [
          "tsconfig.json"
        ],
        "createDefaultProgram": true
      },
      "extends": [
        "plugin:@angular-eslint/recommended",
        "plugin:@angular-eslint/template/process-inline-templates"
      ],
      "rules": {
        "@angular-eslint/directive-selector": [
          "error",
          {
            "type": "attribute",
            "prefix": "app",
            "style": "camelCase"
          }
        ],
        "@angular-eslint/component-selector": [
          "error",
          {
            "type": "element",
            "prefix": "app",
            "style": "kebab-case"
          }
        ]
      }
    },
    {
      "files": ["*.ts"],
      "parser": "@typescript-eslint/parser",
      "parserOptions": {
        "ecmaVersion": 2019,
        "project": "./tsconfig.json",
        "sourceType": "module"
      },
      "extends": ["plugin:rxjs/recommended"]
    },
    {
      "files": ["*.ts"],
      "extends": ["plugin:ngrx/recommended"]
    },
    {
      "files": [
        "*.html"
      ],
      "extends": [
        "plugin:@angular-eslint/template/recommended"
      ],
      "rules": {}
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Before you commit those changes, run ESLint across the codebase with the --fix flag so it can resolve violations on its own.

npx eslint . --fix
Enter fullscreen mode Exit fullscreen mode

That approach clears up a lot of noise, though some issues demand manual rewrites of the offending code. To see every error and warning ESLint reports, run this command.

npx eslint .
Enter fullscreen mode Exit fullscreen mode

If the backlog of errors feels overwhelming, remember Betterer from earlier—its built-in Betterer ESLint Test is a handy way to fold in fixes gradually.

Consistent Formatting with Prettier

Everyone has their own way of writing—and formatting—code, and that variety can slow down code reviews as reviewers wade through formatting noise. Introducing a shared style keeps changes focused on the actual task, so reviews stay clean and meaningful.

Prettier, an opinionated code formatter, is the standard choice for enforcing uniform style.

Getting it set up in your project is straightforward with this command.

npm i --save-dev prettier
Enter fullscreen mode Exit fullscreen mode

Next, add a prettier.config.js file with the formatting options that suit your team, for instance:

module.exports = {
  tabWidth: 2,
  useTabs: false,
  semi: true,
  singleQuote: true,
  trailingComma: 'all',
};
Enter fullscreen mode Exit fullscreen mode

I'd strongly suggest formatting the entire codebase right away. Waiting only means that your next small tweak to a file drags in a pile of unrelated styling changes, muddying the review.

To format everything in one sweep, use this command.

npx prettier . --write
Enter fullscreen mode Exit fullscreen mode

Consolidating the Toolset

When a project has seen multiple contributors, you'll sometimes spot duplicated efforts—several libraries doing the same job, like icon or utility packages. That kind of drift makes it tricky for newcomers to figure out the intended approach and keep the UI consistent.

Part of your job is spotting those overlaps and guiding the project back to a single, preferred library per concern. Even better, documenting recommended practices helps everyone stay aligned.

An added perk? Consolidating libraries often trims the bundle size.

Building Test Coverage

In codebases without tests, touching existing features can feel risky, with every change carrying an unspoken fear of breaking something. Writing end-to-end tests gives you a safety net and a reason to walk through the app, getting familiar with its inner workings.

Start small: one happy-path test covering a critical flow can already pay dividends. It's directly useful and later serves as a foundation others can expand on.

For writing those end-to-end tests, I've been using Playwright. It excels at quick wins—the test generator command records your clicks and turns them into a complete test automatically. It really can be that straightforward.

Later articles will likely cover the finer points of why I lean on Playwright and how to wire it into an Angular project.

Git hooks

The tools and conventions described above can substantially improve the project's health, but they aren't a set-and-forget solution. There is also no mechanism to ensure that every team member, including those who join later, adheres to them.

Merely documenting your efforts and asking everyone to be more diligent rarely works. People may agree wholeheartedly in the moment, but good intentions tend to fade once daily work resumes.

To truly enforce these standards, you need git hooks.
A hook is a script that runs either before (pre) or after (post) a git command is executed.

Most commonly, you'd write a hook for the pre-command phase.
The pre-commit and pre-push hooks are particularly useful for blocking bad code from entering a branch.

In the example below, a pre-commit file is placed inside the .githooks directory, and the hook logic is implemented there.
The hook can invoke any npm script. Here, it runs Betterer with the precommit option, followed by lint-staged.

#!/bin/sh

npx betterer precommit
npx lint-staged

# instead of adding the commands in this file,
# you can also add a script to the package.json scripts to run the desired checks
# npm run pre-commit
Enter fullscreen mode Exit fullscreen mode

To register the hook, you can add the prepare lifecycle script to the package.json file. This script runs automatically when a developer executes npm install, which means the git hook gets set up without any manual steps.

{
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "prepare": "git config core.hookspath .githooks"
  },
  "dependencies": {},
  "devDependencies": {
    "lint-staged": "^12.3.3",
  }
}
Enter fullscreen mode Exit fullscreen mode

lint-staged is another library that helps keep the codebase clean, and it pairs nicely with git hooks.
It allows you to run commands specifically on files that are staged, just before they are committed.
In practical terms, any file that is staged will be automatically formatted (a safety net for anyone whose IDE doesn't run Prettier) and then checked against ESLint as well as any strictness rules. This guarantees consistent formatting and ensures that no violations related to strict compilers or ESLint rules slip through.
Running these checks on the entire project every time would be slow. With lint-staged, you only touch the relevant files, so the process remains nearly instant.

Install lint-staged with the command below.

npm install --save-dev lint-staged
Enter fullscreen mode Exit fullscreen mode

To configure it, create a lint-staged.config.js file and specify the prettier and eslint commands.

module.exports = {
  '*.{ts,js}': ['prettier --write', 'eslint --fix'],
  '*.html': ['prettier --write', 'eslint'],
  '*.{json,md,css}': ['prettier --write'],
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

Starting on a new team is always an exciting prospect, and you never know what state the application will be in.
To set things in motion on a good note, there are likely some maintenance tasks that others have been putting off, which you can readily take on.

If you pay attention to what your colleagues complain about and inspect the code closely, you're bound to find areas that need work. No codebase is flawless.
When you spot such a case, my advice is simple: "be proactive," and get started.

This approach helps you in multiple ways: you learn the ins and outs of the application and build rapport with your teammates while making a tangible difference from day one.
The team as a whole also reaps the rewards. A tidy and well-thought-out codebase lifts overall morale, which in turn encourages continuous improvements. That boost in team spirit often translates into higher productivity, which is a win for management as well.

Enjoy the process, and keep your workspace clean!


Follow me on Twitter at @tim_deschryver | Subscribe to the Newsletter | Originally published on timdeschryver.dev.