TypeScript strictly typed – Part 1: configuring a project

RMAG news

After the introduction of this posts series, we are going to the topic’s technical core. First, we will talk about configuration, and why it is very important to do it at the very beginning of a project.

When to enable strict options?

I cannot insist more on the fact that strict options must be enabled at the very beginning of any project. Doing so is an easy and straightforward process: one just has to gradually type correctly when coding.

But enabling strict options in an on-going project is a completely different matter: even if TypeScript default mode is capable of inferring types in the majority of cases, the remaining untyped places are a proportion of the codebase. So the more code, the more places to fix, and it requires a clear understanding of what each code is doing.

It is one of the main recurring big errors I have seen in all the companies I have helped over the last decade as a TypeScript expert. Recovering from it is good but really time consuming and painful.

So be sure to always check a new project is in strict mode before to start coding.

Frameworks status

TypeScript strict mode is enabled automatically when generating a project with the last versions of:

TypeScript: tsc –init

Angular: npm init @angular@latest

React App: npx create-react-app –template typescript

Next.js: npx create-next-app@latest

Vue: npm create vue@latest, then choosing TypeScript

Note it has not always been the case with older versions of these frameworks.

Full configuration (the hard way)

We will see in the next parts that the “strict” mode is not enough. Other TypeScript compiler options must be enabled, as well as some lint rules.

A complete configuration would look like this:

For TypeScript, in tsconfig.json:

{
compilerOptions: {
strict: true,
exactOptionalPropertyTypes: true,
noPropertyAccessFromIndexSignature: true,
noUncheckedIndexedAccess: true
}
}

For ESLint + TypeScript ESLint, with the new flat config eslint.config.js:

export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
eqeqeq: error,
prefer-arrow-callback: error,
prefer-template: error,
@typescript-eslint/explicit-function-return-type: error,
@typescript-eslint/no-explicit-any: error,
@typescript-eslint/no-non-null-assertion: error,
@typescript-eslint/no-unsafe-argument: error,
@typescript-eslint/no-unsafe-assignment: error,
@typescript-eslint/no-unsafe-call: error,
@typescript-eslint/no-unsafe-member-access: error,
@typescript-eslint/no-unsafe-return: error,
@typescript-eslint/prefer-for-of: error,
@typescript-eslint/prefer-nullish-coalescing: error,
@typescript-eslint/prefer-optional-chain: error,
@typescript-eslint/restrict-plus-operands: [error, {
allowAny: false,
allowBoolean: false,
allowNullish: false,
allowNumberAndString: false,
allowRegExp: false,
}],
@typescript-eslint/restrict-template-expressions: error,
@typescript-eslint/strict-boolean-expressions: [error, {
allowNumber: false,
allowString: false,
}],
@typescript-eslint/use-unknown-in-catch-callback-variable: error,
},
});

For ESLint + TypeScript ESLint, with the legacy config in .eslintrc.json:

{
“parserOptions”: {
“project”: true
},
“extends”: [
“eslint:recommended”,
“plugin:@typescript-eslint/strict-type-checked”,
“plugin:@typescript-eslint/stylistic-type-checked”
],
“rules”: {
“eqeqeq”: “error”,
“prefer-arrow-callback”: “error”,
“prefer-template”: “error”,
“@typescript-eslint/explicit-function-return-type”: “error”,
“@typescript-eslint/no-explicit-any”: “error”,
“@typescript-eslint/no-non-null-assertion”: “error”,
“@typescript-eslint/no-unsafe-argument”: “error”,
“@typescript-eslint/no-unsafe-assignment”: “error”,
“@typescript-eslint/no-unsafe-call”: “error”,
“@typescript-eslint/no-unsafe-member-access”: “error”,
“@typescript-eslint/no-unsafe-return”: “error”,
“@typescript-eslint/prefer-for-of”: “error”,
“@typescript-eslint/prefer-nullish-coalescing”: “error”,
“@typescript-eslint/prefer-optional-chain”: “error”,
“@typescript-eslint/restrict-plus-operands”: [“error”, {
“allowAny”: false,
“allowBoolean”: false,
“allowNullish”: false,
“allowNumberAndString”: false,
“allowRegExp”: false
}],
“@typescript-eslint/restrict-template-expressions”: “error”,
“@typescript-eslint/strict-boolean-expressions”: [“error”, {
“allowNumber”: false,
“allowString”: false
}],
“@typescript-eslint/use-unknown-in-catch-callback-variable”: “error”
}
}

For Biome, in biome.json:

{
“linter”: {
“enabled”: true,
“rules”: {
“recommended”: true,
“style”: {
“useForOf”: “error”
}
}
}
}

Note that Biome is still a recent tool and that not all the lint rules we will discuss exist yet.

Also note that these configuration examples only include options and rules related to this posts series topic. Other options and rules may be added to enforce other TypeScript good practices not related to strict typing.

For all tools, be careful if a configuration extends another one. It could mean that even if a preset like strict is enabled, one of the individual option included in the preset is disabled in the parent configuration. So in this case, all options should be enabled individually.

Automatic configuration (the easy way)

Totally optional, but if one does not want to lose time to remember and configure all these options manually, one can run this command:

npx typescript-strictly-typed@latest

It is a very simple tool I published to automatically add all the strict options in a TypeScript project.

Next part

In the next part of this posts series, we will explain and solve the first problem of TypeScript default behavior: from partial to full coverage typing.

You want to comment or contact me? Instructions are available in the summary.

Please follow and like us:
Pin Share