DEV Community

Cover image for TypeScript strictly typed - Part 1: configuring a project
Cyrille Tuzi
Cyrille Tuzi

Posted on • Updated on

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/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/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.

Top comments (2)

Collapse
 
dandv profile image
Dan Dascalescu • Edited

Thank you for starting this important conversation! I was surprised to see that typescript-eslint with strict-type-checked didn't catch missing returns, which "strict": true in tsconfig.json will. Could you expand in a future post about the differences and overlap between TypeScript's type checking/compiler options, and typescript-eslint? I found this SO answer lacking.

Another question: what exactly is the "strict mode family options". That documentation section claims

Turning this on is equivalent to enabling all of the strict mode family options, which are outlined below.

but it only outlines strictBindCallApply, strictFunctionTypes, strictNullChecks, strictPropertyInitialization and useUnknownInCatchVariables. It doesn't list noImplicitReturns.

Collapse
 
cyrilletuzi profile image
Cyrille Tuzi

Hello Dan, thank you for your feedback.

The linter (in this case, TypeScript ESLint) is here to add additional rules for things which are not checked by the compiler. So generally, there is no overlap between TypeScript compiler options and TypeScript ESLint rules.

I think you may be confusing two different checks. noImplicitReturns (in TypeScript compiler options) just checks if all code paths in a function return a value (which generally happens when there are conditions, leading to different cases). But it does not force to explicit the return type of functions, which is the role of the @typescript-eslint/explicit-function-return-type lint option discussed in this posts series.

The "strict" compiler option is just a convenient option to enable several sub-options at once. But indeed, it does not include all sub-options related to strictness. Why some rules are included or not in "strict" is up to the TypeScript team and I cannot not answer for them. From what I know, it is sometimes for backward compatibility with existing libraries, and sometimes because some options are considered too advanced good practices to follow for beginners.