TypeScript strictly typed - Part 1: configuring a project

cyrilletuzi

Cyrille Tuzi

Posted on June 12, 2024

TypeScript strictly typed - Part 1: configuring a project

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.

We will cover:

  • When to enable strict options?
  • Frameworks status
  • Full configuration (the hard way)
  • Automatic configuration (the easy way)

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
  • Deno: by default

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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/no-unsafe-type-assertion": "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",
    },
});
Enter fullscreen mode Exit fullscreen mode

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/no-unsafe-type-assertion": "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"
  }
}
Enter fullscreen mode Exit fullscreen mode

For Biome, in biome.json:

{
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "style": {
        "useForOf": "error"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

For Deno, in deno.json:

{
  "compilerOptions": {
    "exactOptionalPropertyTypes": true,
    "noPropertyAccessFromIndexSignature": true,
    "noUncheckedIndexedAccess": true,
    "useUnknownInCatchVariables": true
  },
  "lint": {
    "rules": {
      "tags": [
        "recommended"
      ],
      "include": [
        "eqeqeq",
        "explicit-function-return-type",
        "no-non-null-assertion"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

It is a 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 contact me? Instructions are available in the summary.

💖 💪 🙅 🚩
cyrilletuzi
Cyrille Tuzi

Posted on June 12, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related