DEV Community

Vladyslav Zubko
Vladyslav Zubko

Posted on • Edited on • Originally published at whatislove.dev

The Strictest TypeScript Config

Most people know that to make projects using TypeScript "strict" in terms of typing, you need to enable the "strict": true option in the tsconfig. What this option does is enable a multitude of other options, such as alwaysStrict, strictNullChecks, strictBindCallApply, strictFunctionTypes, strictPropertyInitialization, noImplicitAny, noImplicitThis, useUnknownInCatchVariables. Indeed, all these options will undoubtedly make your TypeScript project stricter.

However, not everyone knows that in the TypeScript config, there are other options that can more reliably protect your project from type-related errors, which are not yet automatically included by the strict option. Let's take a look at these options together to make our projects using TypeScript more robust.

noUncheckedIndexedAccess

The first option we will explore is called noUncheckedIndexedAccess. Historically, TypeScript automatically infers a type if it has grounds to do so. For example, if we manually specify the type of an array while leaving it empty or use an index signature for objects. It might seem like TypeScript is doing us a favor, but very often, this can lead to unintended consequences.

This is where noUncheckedIndexedAccess comes to the rescue. It adds undefined to any undeclared key of an object or index of an array, prompting additional checks to ensure that you are fully aware of what you are doing.

// noUncheckedIndexedAccess: false (default behavior)

const ids: number[] = [];
const user: Record<string, string> = {};

const id0 = ids[0]; // number
const username = user.username // string

id0.toFixed(); // βœ… but πŸ’₯
username.trim(); // βœ… but πŸ’₯

// noUncheckedIndexedAccess: true

const id0 = ids[0]; // number | undefined
const username = user.username // string | undefined

id0.toFixed(); // ❌ Error: 'id0' is possibly 'undefined'.
username.trim(); // ❌ Error: 'username' is possibly 'undefined'.
Enter fullscreen mode Exit fullscreen mode

exactOptionalPropertyTypes

Next, we have the option exactOptionalPropertyTypes, which also addresses a long-standing pain point in TypeScript. Specifically, the issue that the ? prefix in object keys serves two purposes – indicating that a field can be undefined and also signifying that the field might not exist in the object at all (yes, these are two different things).

With this option enabled, TypeScript retains only one behavior with the ? prefix. Now, it signifies only the absence of a key, not that it could be undefined. If you need to allow the value to be undefined, you must specify it explicitly (e.g., hasFriends: string | undefined).

// exactOptionalPropertyTypes: false (default behavior)

type User = {
    name: string;
    age: number;
    hasFriends?: boolean;
};

const user: User = {
    name: 'John',
    age: 18,
};

user.hasFriends = true; // βœ…
user.hasFriends = undefined; // βœ…

// exactOptionalPropertyTypes: true

user.hasFriends = true; // βœ…
user.hasFriends = undefined; // ❌
Enter fullscreen mode Exit fullscreen mode

noPropertyAccessFromIndexSignature

Concluding the trio of significant options, which are not part of the strict option but play a crucial role in enforcing stricter typing in TypeScript projects, is the noPropertyAccessFromIndexSignature option.

There is often a temptation to either write your own index signature for an object, even when it seems like there is nothing preventing you from explicitly describing all the keys of the object, or to use index signatures from libraries, as library authors often indulge in them. noPropertyAccessFromIndexSignature requires the use of bracket notation when accessing keys of objects described through index signatures.

The goal of this option is to make sure you are absolutely certain about what you are doing. Since nobody likes bracket notation where it can be avoided, this option forces you to avoid index signatures as much as possible. Indeed, many problems arise from them, and this option aims to mitigate those issues.

// noPropertyAccessFromIndexSignature: false (default behavior)

type Settings = {
  mode: string;
  [key: string]: string;
};

const settings: Settings = {
  mode: 'MFA',
  kind: 'google',
};

settings.mode // βœ…
settings.kind // βœ…
settings.wat // βœ…

// noPropertyAccessFromIndexSignature: true

settings.mode // βœ…
settings.kind // ❌
settings.wat // ❌
settings['kind'] // βœ…, but it's better to avoid index signatures and describe all type keys explicitly
settings['wat'] // βœ…
Enter fullscreen mode Exit fullscreen mode

By the way, the TypeScript ESLint plugin developers have thoughtfully provided the ability to use this and many other options from the TypeScript config, along with their rules. The @typescript-eslint/dot-notation rule syncs seamlessly with this option, once again emphasizing that using index signatures is not a good practice.

Conclusion

There are also other options improving typing in the TypeScript config that are not automatically enabled with the strict option, but they are not as crucial as the three mentioned above.

For all new projects where you will be using TypeScript, I recommend starting with a TypeScript config where the strict option is enabled along with the additional three options we discussed earlier: noUncheckedIndexedAccess, exactOptionalPropertyTypes, and noPropertyAccessFromIndexSignature.

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noPropertyAccessFromIndexSignature": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Or try enabling them in an existing project, but be prepared to uncover many areas where you might have taken shortcuts πŸ˜‰

All these options, by imposing additional constraints and promoting more correct coding practices, will help make your project more reliable. This simplifies both its maintenance in the long run and the quicker detection of incorrect behavior in your code.

Top comments (12)

Collapse
 
artxe2 profile image
Yeom suyun

It's quite fascinating. However, noUncheckedIndexedAccess can be a bit restrictive, which is a bit disappointing.

For instance, it raises errors in cases like the following:

for (let key of obj) {
  obj[key].func(); // possibly 'undefined'
}
if (obj[key]) {
  obj[key].func(); // possibly 'undefined'
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
what1s1ove profile image
Vladyslav Zubko • Edited

Hey @artxe2 !

I think the issue lies more in the for...of/for...in loop, as it returns a string for a key variable. If you typecast it, everything will be fine πŸ™‚

for (let key of Object.keys(obj)) {
  const typeKey = key as keyof typeof obj;

  obj[typeKey].func(); // βœ…


  if (obj[typeKey]) {
    obj[typeKey].func(); // βœ…
  }
}

for (let key in obj) {
  const typeKey = key as keyof typeof obj;

  obj[typeKey].func(); // βœ…


  if (obj[typeKey]) {
    obj[typeKey].func(); // βœ…
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
artxe2 profile image
Yeom suyun

The solution you provided might not resolve the issue.
TS Playground

var obj: Record<string, { func: Function }> = {}

for (let key of Object.keys(obj)) {
  const value = obj[key]
  if (value) {
    value.func();
  }
}

for (let key in obj) {
  const value = obj[key] as { func: Function }
  value.func();
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
what1s1ove profile image
Vladyslav Zubko

Oh, I see.

I understand what issue you're talking about. But here again, the problem is not with noUncheckedIndexedAccess, but with index signatures. If you explicitly annotate all keys, there won't be such an error. Here is the same playground but this explicit keys.

In my article, I repeatedly mentioned that the index signature is a bad thing 😁

Thread Thread
 
artxe2 profile image
Yeom suyun

Yes, indeed.
If you have been using the strict option in TypeScript, applying additional stricter options may not introduce many new errors.
However, for types like Array or Record, there are clear use cases, and it's not accurate to categorize them as inherently bad.
For instance, I found type casting cumbersome in cases like the code below to eliminate errors.

/**
 * @param {string} text
 * @returns {string}
 */
let _default = text => {
  let regex = ""
  let len = text.length - 1
  for (let i = 0; i < len; i++) {
    let t = /** @type {string} */(text[i])/**/
    regex += first_consonant_letters[t] ?? get_letter_range(t)
  }
  return regex + get_letter_range(
    /** @type {string} */(text[len])/**/
  )
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
what1s1ove profile image
Vladyslav Zubko

Agreed!
In general, as always, it depends on the situation 😁

Thread Thread
 
bwca profile image
Volodymyr Yepishev

Just dropping my 2 cents, the possible undefined problem of the aforementioned record can be solved by an explicit cast in the value lookup, it's just that both the object and its key need to be casted.

let obj: Record<string, { func: Function }> = {}

for (let key of Object.keys(obj)) {
  (obj[key] as typeof obj[typeof key]).func(); // βœ…
}
Enter fullscreen mode Exit fullscreen mode

Though the case seems rather artificial, as you'd probably be using .values or .entries :P

Thread Thread
 
what1s1ove profile image
Vladyslav Zubko • Edited

Hey @bwca !
Thank you for sharing your approach to solving the problem!
I believe it's challenging to assess it solely based on this small code snippet. In real projects, everything can be significantly different πŸ₯²

Collapse
 
denys_dev profile image
denkochev

Wow, hey from kottans nice to see you here :D

Collapse
 
what1s1ove profile image
Vladyslav Zubko

Hi @denys_dev ☺️
Nice to see you and I'm so glad you keep learning πŸ’ͺ

Collapse
 
lizaveis profile image
Yelyzaveta Veis

Thank you for the article πŸ™ƒ

Collapse
 
what1s1ove profile image
Vladyslav Zubko

Thank you for not standing still!