0014 - Adopt Typescript Strict flag
ID: | ADR-0014 |
---|---|
Status: | ACCEPTED |
Published: | 2022-09-02 |
Context and Problem Statement
For compatibility reasons since the codebase was originally written in JavaScript we have never been able to use the TypeScript strict flag. This has lead to several issues with bugs and resulted in a lower code quality.
TypeScript Strict
The
https://www.typescriptlang.org/tsconfig#strictstrict
flag enables a wide range of type checking behavior that results in stronger guarantees of program correctness. Turning this on is equivalent to enabling all of the strict mode family options.
Of particular note is the Strict Null Checks. By default TypeScript will ignore false
,
null
and undefined
allowing them to be used for any types. When strictNullChecks
are enabled
they must be explicitly allowed in the type definition.
Typescript strict mode plugin
The Typescript strict mode plugin is a TypeScript plugin that allows you to progressively turn on strict mode on a file by file basis. This will allow us to progressively update our codebase to support strict mode without requiring a significant initial engineering effort.
Considered Options
- Keep strict disabled - We will continue to run into null issues, and code quality will be lower than it could be.
- Enable strict flag immediately - Turn on the strict flag immediately and assign an engineer to resolve all errors caused by it. This will take a long time, and result in a multitude of conflicts.
- Progressively enable strict flag - Use the TypeScript plugin to migrate files one by one.
Decision Outcome
Chosen option: Progressively enable strict flag.
Positive Consequences
- We can immediately start using the strict flag.
- Lower initial developer effort.
Negative Consequences
- Migration will be slower.
- We will continue to have parts of the code with null issues.
- Might lead to false security if a file is strict but uses non-strict dependencies.
- Few best practices.
Files to be updated
- libs/common: 310
- models: 174
- services: 40
- abstractions: 45
- libs/angular: 64
- libs/node: 11
- libs/electron: 10
- apps: 309
- web: 163
- browser: 80
- desktop: 30
- cli: 36
Plan
There are several potential pitfalls related to progressively enabling strict mode. Which will benefit from having a plan on how we should tackle the migration. Each category of files should have a proper example how how they should be migrated. And during the initial migration period a best effort should be made to be strict-compliant, but we should not prevent people from adding new files that are not strict. What we should prevent is people adding new non-strict files in the category that we are currently migrating.
Preparation
Enable the plugin and run update-strict-comments
which adds the //@ts-strict-ignore
comment to
all files which produce a strict error.
Transition
- Migrate
libs/common/models
. - Migrate
libs/common/services
andlibs/common/abstractions
. - Migrate remaining
libs/common
. - Migrate
libs/angular
. - Migrate apps. (And forbid anyone from adding new non-strict files)
Guidelines
Below are some helpful guidelines for migrating files to strict mode.
Avoid null, prefer undefined
The strictNullChecks
flag essentially requires that we add | null
to almost all our types. This
is quite annoying and a better approach is to use undefined
instead. Which allows us to use the
?
operator to define the fields as optional. This essentially adds a | undefined
to the type.
For some additional context behind this discussion, please read
Guidelines for choosing between null
and undefined
with strictNullChecks
· Issue #9653 · microsoft/TypeScript.
Use find references / search
During the migration strict mode will only be allowed on a subset of the files. This means that changing the interface from null to undefined may result in places that still use undefined. Please double check that null is removed to ensure the expected behavior.
Avoid making non-optional fields optional
It may be tempting to just add a ?
to all fields while migrating the code. Please think carefully
wether the field should actually be optional or not. Arrays for example should almost never be
undefined
and a more sensible default is []
.
Use optional chaining
Take advantage of optional chaining to avoid null checks. Optional chaining allows you to rewire the following code.
// Avoid
if (obj == null || obj.field == null) {
return false;
}
return true;
// Prefer
return obj?.field != null;
Use Nullish coalescing operator (??
)
const foo = null ?? "default string";
Use !
on required @Input
parameters
Typescript's property initialization has no knowledge of Angular's guarantees. That means that
@Input({ required: true }) options: WebAuthnOptions;
will error as options
is not initialized in the constructor. The suggested fix is to use the
non-null assert operator, !
.
@Input({ required: true }) options!: WebAuthnOptions;
Once required input signals are out of dev preview for us (Angular 19), we can transition to using them and initialize these kinds of parameters in the class body or constructor.