20 TypeScript Flags You Should be Using

Do this if you use TypeScript!

LinkIntroduction

Switching from JavaScript to TypeScript is already a huge improvement in any codebase, but we can do better. We can go beyond the default TypeScript checks and make our codebase much safer.

To do that, we'll need to set some options for the TypeScript compiler. In this article, I'll show you 20 TypeScript compiler options that my team and I use and recommend to our clients. Then, we'll go over each of those flags to understand what they do and why we recommend them.

I'm Lucas Paganini, and in this site, we release web development tutorials. Join our newsletter if you're interested in that.

LinkDealing with the Errors

I'll show you the flags in a second, but first, a warning. Sometimes, just knowing what to do is not enough. For example, if you're working in a big codebase, enabling those flags will probably cause a lot of compiler errors.

My team and I have been there. Most of our clients hire us to work on Angular projects (which use TypeScript). So trust me when I say that we understand the challenge of tackling tech debt in big legacy codebases.

So besides this article, we're also working on another article that will go over our techniques to deal with the compiler errors that arise from enabling each of those flags in a legacy codebase.

LinkOur Compiler Options Recommendation

Without further ado, these are the 20 TypeScript compiler options that we recommend:

TypeScript
{
  "compilerOptions": {

    // Strict mode (9)
    "strict": true,
    "alwaysStrict": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "strictBindCallApply": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "useUnknownInCatchVariables": true,

    // No unused code (4)
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "allowUnusedLabels": false,
    "allowUnreachableCode": false,

    // No implicit code (2)
    "noImplicitOverride": true,
    "noImplicitReturns": true,

    // Others (5)
    "noUncheckedIndexedAccess": true,
    "noPropertyAccessFromIndexSignature": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": true,
    "forceConsistentCasingInFileNames": true

  }
}

Of course, a real tsconfig.json would also have other flags, such as target, outDir, sourceMap, etc. But we're only interested in the type checking flags. We grouped them into 4 categories:

  1. Strict mode
  2. No unused code
  3. No implicit code
  4. Others

LinkStaying Updated

Beware, as TypeScript evolves, new flags will be created. Maybe there is a new flag that didn't exist by the time we wrote this article, so along with this list, we also recommend that you take a look at the TypeScript documentation to see if they have any other flags that might interest you.

👉 Another way of staying updated is to subscribe to our newsletter. Just saying...

LinkStrict Mode Flags

Let's start by breaking down the strict mode flags. As you can see, there are 9 flags in the strict mode category:

  1. strict
  2. alwaysStrict
  3. noImplicitAny
  4. noImplicitThis
  5. strictNullChecks
  6. strictBindCallApply
  7. strictFunctionTypes
  8. strictPropertyInitialization
  9. useUnknownInCatchVariables

strict

Description

The first one, strict, is actually just an alias. Setting strict to true is the same as setting all the other 8 strict mode flags to true. It's just a shortcut.

TypeScript
{
  "compilerOptions": {

    "strict": true,
  }
}
TypeScript
{
  "compilerOptions": {

    "alwaysStrict": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "strictBindCallApply": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "useUnknownInCatchVariables": true,
  }
}

Motivation

But that's not to say that it doesn't provide value. As I've said before, TypeScript is evolving. If future versions of TypeScript include new flags to the strict category, they will be enabled by default due to this alias.

TypeScript
{
  "compilerOptions": {

    "alwaysStrict": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "strictBindCallApply": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "useUnknownInCatchVariables": true,

    "someFlagFromTheFuture": true,
    "someOtherFlagFromTheFuture": true,
    "yetAnotherFlagFromTheFuture": true,
  }
}

alwaysStrict

Description

JavaScript also has a strict mode, it was introduced in ES5, a long time ago. To parse a JavaScript file in strict mode, you just need to write "use strict" at the top of your js file, before any other statements.

JavaScript
'use strict';
var str = 'Hello World';

Enabling alwaysStrict in the TypeScript compiler ensures that your files are parsed in the JavaScript strict mode and that the transpiled files have "use strict" at the top.

Motivation

The JavaScript strict mode is all about turning mistakes into errors. For example, if you misspell a variable, you expect an error to happen. But if you're running JavaScript in "sloopy mode", that won't throw an error. Instead, it will set a global variable.

JavaScript
'use strict';
mistypeVariable = 17;
//=> ⚠️ RUNTIME ERROR: mistypeVariable is not defined
JavaScript
// 'use strict';
mistypeVariable = 17;
//=> ✅ OK, setting window.mistypeVariable to 17

The benefit of enabling alwaysStrict in TypeScript is that those runtime errors you'd get from the JavaScript strict mode are turned into compiler errors instead.

TypeScript
mistypeVariable = 17;
//=> ❌ COMPILER ERROR: Cannot find name 'mistypeVariable'

noImplicitAny

Description

noImplicitAny is pretty self-explanatory. It won't allow your code to be implicitly inferred as any. So, if you explicitly cast a type to any, that's fine. But if TypeScript implicitly infers that something is any, you'll get a compiler error.

TypeScript
function explicit(explicitAny: any) {}
//=> ✅ OK: We are explicitly setting it to 'any'

function implicit(implicitAny) {}
//=> ❌ COMPILER ERROR: Parameter 'implicitAny' implicitly has an 'any' type

Motivation

The benefit here is that inference issues won't go unnoticed. I'll give you two different examples where this flag would save you:

1 - Parameter types

First, let's say you declare a function to split a string by dots. Your function should receive a string and return an Array<string>. But you forget to set your parameter type to string, what happens now?

The parameter type will be set to any by default and TypeScript won't even warn you.

TypeScript
// noImplicityAny: false ❌

function splitByDots(value) {
  //=> value: any
  //=> returns: any
  return value.split('.');
}

Here you are, thinking that you're leveraging all the power of TypeScript, but your function has no type-safety whatsoever. TypeScript will allow other developers to use your function with whatever types they want. TypeScript will say it's ok to pass a number to your function, or a Date, or an airplane.

TypeScript
// noImplicityAny: false ❌

function splitByDots(value) {
  //=> value: any
  //=> returns: any
  return value.split(".");
}

splitByDots("www.lucaspaganini.com");
// "All good here, I'm happy"

splitByDots(123);
// "Wait, what??"

splitByDots(new Date());
// "TypeScript, why are you allowing that?"

splitByDots(✈️);
// "How's that even possible?"

2 - Inferred variable types

But it doesn't stop there. Let's go to the second example. Even if you do pass a string to your function, instead of an airplane, you can still have issues.

Let's say you call your function and save the returned value in a variable. You think that your variable is an Array<string>, but it's actually any. If you try to call Array.forEach and misspell it, TypeScript will let you shoot yourself in the foot, because it doesn't know that your variable is an Array.

TypeScript
// noImplicityAny: false ❌

function splitByDots(value) {
  //=> value: any
  //=> returns: any
  return value.split(".");
}

const words = splitByDots("www.lucaspaganini.com");
//=> words: any

words.foreach(...)
//=> We mistyped Array.forEach and TypeScript gave us no warning 😔

Enabling noImplicitAny would raise a compiler error in your function declaration, forcing you to set the type of your parameter and preventing those inference issues to go unnoticed.

TypeScript
// noImplicityAny: true ✅

function splitByDots(value) {
  //=> ❌ COMPILER ERROR: Parameter 'value' implicitly has an 'any' type
  //=> value: any
  //=> returns: any
  return value.split(".");
}

const words = splitByDots("www.lucaspaganini.com");
//=> words: any

words.foreach(...)
TypeScript
// noImplicityAny: true ✅

function splitByDots(value: string) {
  //=> value: string
  //=> returns: Array<string>
  return value.split(".");
}

const words = splitByDots("www.lucaspaganini.com");
//=> words: Array<string>

words.foreach(...)
//=> ❌ COMPILER ERROR: Property 'foreach' does not exist on type 'Array<string>'. Did you mean 'forEach'?

And if you really want your parameter to be of type any, you can explicitly type it as any.

TypeScript
// noImplicityAny: true ✅

function splitByDots(value: any) {
  //=> value: any
  //=> returns: any
  return value.split('.');
}

noImplicitThis

Description

"noImplictThis" is similar, it will raise an error if this is implicitly any.

Motivation

The value of this in JavaScript is contextual, so if you're not 100% confident with how JavaScript sets this, you might write a class like the following and not notice your error:

TypeScript
class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  getName() {
    return function () {
      return this.name;
    };
  }
}

const user = new User('Tom');

user.getName()();

The issue here is that this, in this.name, does not refer to the class instance. So, calling user.getName()() will raise a runtime error.

TypeScript
class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  getName() {
    return function () {
      return this.name;
    };
  }
}

const user = new User('Tom');

user.getName()();
//=> ❌ RUNTIME ERROR: Cannot read property 'name' of undefined

To prevent that, we can enable noImplictThis, which would raise a compiler error, preventing you from using this if it's inferred to be any by TypeScript.

TypeScript
class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  getName() {
    return function () {
      return this.name;
      //=> ❌ COMPILER ERROR: 'this' implicitly has type 'any' because it does not have a type annotation.
    };
  }
}

const user = new User('Tom');

user.getName()();

strictNullChecks

Description

By default, TypeScript ignores null and undefined, you can change that by enabling strictNullChecks. That's by far the most valuable flag of all. It will rightfully force you to deal with null and undefined.

Motivation

Think about this for a second. You're calling Array.find and expecting to receive an element of the array. But what if you couldn't find what you were looking for? It will return undefined. Are you dealing with that? Or will your code break during runtime?

TypeScript
const users = [{ name: 'Bob', age: 13 }];

const user = users.find((u) => u.age < 10);

console.log(user.name);
//=> ❌ COMPILER ERROR: Object is possibly 'undefined'.

We should obviously always deal with null and undefined. I get that you'll have millions of errors when you enable that flag, but if you're just starting a new project, this is a no-brainer. You should definitely have it enabled.

strictBindCallApply

Description

strictBindCallApply enforces the correct types for function call, bind and apply.

Motivation

I don't generally use call, bind, or apply, but if I were to use it, I'd like it to be correctly typed. I don't even know why that's an option, they should be correctly typed by default.

TypeScript
const fn = (x: string) => parseInt(x);

fn.call(undefined, '10');
//=> ✅ OK

fn.call(undefined, false);
//=> ❌ COMPILER ERROR: Argument of type 'boolean' is not assignable to parameter of type 'string'.

strictFunctionTypes

Description

strictFunctionTypes causes function parameters to be checked more correctly. I know, that sounds weird, but that's exactly what it does.

By default, TypeScript function parameters are bivariant. That means that they are both covariant and contravariant.

Explaining Code Variance

Explaining variance is a topic on its own, but basically, when you're able to assign a broader type to a more specific one, that's contravariance. When you're able to assign a specific type to a broader one, that's covariance. When you go both ways, that's bivariance. For example, take a look at this code:

TypeScript
interface User {
  name: string;
}

interface Admin extends User {
  permissions: Array<string>;
}

declare let admin: Admin;
declare let user: User;

We're declaring an interface called User that has a name and an interface called Admin that extends the User interface and adds an Array of permissions. Then we declare two variables: one is an Admin and the other is a User.

From that code, I'll give you examples of covariance and contravariance.

Covariance

As I've said, when you're able to assign a specific type to a broader one, that's covariance. An easy example would be assigning admin to user.

TypeScript
interface User {
  name: string;
}

interface Admin extends User {
  permissions: Array<string>;
}

declare let admin: Admin;
declare let user: User;

// Example of Covariance
user = admin; // ✅ OK
admin = user; // ❌ Error

An Admin is more specific than a User. So, assigning an Admin to a variable that was expecting a User is an example of covariance.

Contravariance

Contravariance would be the opposite, being able to set a broader type to a more specific one. Functions are a good place to find contravariance.

For example, let's define a function to get the name of a User and another to get the name of an Admin.

TypeScript
interface User {
  name: string;
}

interface Admin extends User {
  permissions: Array<string>;
}

let getAdminName = (admin: Admin) => admin.name;
let getUserName = (user: User) => user.name;

// Example of Contravariance
getAdminName = getUserName; // ✅ OK
getUserName = getAdminName; // ❌ Error (with strictFunctionTypes = ✅true)

getUserName is broader than getAdminName. So, assigning getUserName to getAdminName is an example of contravariance.

Bivariance

Again, bivariance is when you have both covariance and contravariance.

TypeScript
interface User {
  name: string;
}

interface Admin extends User {
  permissions: Array<string>;
}

let getAdminName = (admin: Admin) => admin.name;
let getUserName = (user: User) => user.name;

// Example of Bivariance
getAdminName = getUserName; // ✅ OK
getUserName = getAdminName; // ✅ OK (with strictFunctionTypes = ❌false)

Back to our Flag

After that super brief explanation of variance, let's get back to our strictFunctionTypes flag.

As I've said, TypeScript function parameters are bivariant. But that's wrong. As we've seen before, most of the time, function parameters should be contravariant, not bivariant. I can't even imagine a good example of bivariance in function parameters.

So when I said that strictFunctionTypes causes function parameters to be checked more correctly, what I meant is that TypeScript will not treat function parameters as bivariant if you enable this flag. After explaining this flag, the next ones will be a breeze.

strictPropertyInitialization

Description

strictPropertyInitialization makes sure you initialize all of your class properties in the constructor. For example, if we create a class called User and say that it has a name that is a string, we need to set that name in the constructor.

TypeScript
// ❌ Wrong
class User {
  name: string;
  //=> ❌ COMPILER ERROR: Property 'name' has no initializer and is not definitely assigned in the constructor.

  constructor() {}
}
TypeScript
// ✅ Solution 1
class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

If we can't set it in the constructor, we need to change its type to say that name can be either a string or undefined.

TypeScript
// ✅ Solution 2
class User {
  name: string | undefined;

  constructor() {}
}

Motivation

The motivation for this flag is the same as the one for strictNullChecks, we need to deal with undefined, not just ignore it and hope for the best.

useUnknownInCatchVariables

Description

The last strict mode flag is useUnknownInCatchVariables. This flag will implicitly type a variable in a catch clause as unknown, instead of any. This is much safer because using unknown forces us to narrow our type before any operations.

TypeScript
// useUnknownInCatchVariables = ❌false

try {
  // Some code...
} catch (err) {
  //=> err: any
  console.log(err.message);
}
TypeScript
// useUnknownInCatchVariables = ✅true

try {
  // Some code...
} catch (err) {
  //=> err: unknown
  console.log(err.message);
  //=> ❌ COMPILER ERROR: Object is of type 'unknown'
}
TypeScript
// useUnknownInCatchVariables = ✅true

try {
  // Some code...
} catch (err) {
  //=> err: unknown
  if (err instanceof Error) {
    console.log(err.message); //=> err: Error
  }
}

👉 Check this one-minute video that I made explaining the differences between any and unknown.

Motivation

For example, it's fairly common to expect that our catch variable will be an instance of Error, but that's not always the case. JavaScript allows us to throw anything we want. We might throw a string instead of an Error.

That becomes problematic when we try to access properties that only exist in an Error, such as .message.

TypeScript
// useUnknownInCatchVariables = ❌false

try {
  throw Error('error message');
} catch (err) {
  console.log(err.message);
  //=> LOG: 'error message'
}
TypeScript
// useUnknownInCatchVariables = ❌false

try {
  throw 'error message';
} catch (err) {
  console.log(err.message);
  //=> LOG: undefined
}

TypeScript will let you do whatever you want, because by default, catch variables are typed as any. But if we enable this flag, that code will break, because TypeScript will type catch variables as unknown, forcing us to type-check that our variable is indeed an Error instance before accessing the .message property.

TypeScript
// useUnknownInCatchVariables = ✅true

try {
  throw 'error message';
} catch (err) {
  //=> err: unknown
  console.log(err.message);
  //=> ❌ COMPILER ERROR: Object is of type 'unknown'
}
TypeScript
// useUnknownInCatchVariables = ✅true

try {
  throw 'error message';
} catch (err) {
  //=> err: unknown
  if (err instanceof Error) {
    console.log(err.message); //=> err: Error
  }

  if (typeof err === 'string') {
    console.log(err); //=> err: string
  }
}

LinkNo Unused Code Flags

Now we'll get into the no unused code flags. There are 4 in this category:

  1. noUnusedLocals
  2. noUnusedParameters
  3. allowUnusedLabels = false
  4. allowUnreachableCode = false

As you'll see, those flags are more like linting checks than compilation checks.

noUnusedLocals

Description

The first one, noUnusedLocals, will emit an error if there are unused local variables.

TypeScript
// noUnusedLocals = ❌false

const getUserName = (): string => {
  const age = 23;
  return 'Joe';
};
TypeScript
// noUnusedLocals = ✅true

const getUserName = (): string => {
  const age = 23;
  //=> 💡 COMPILATION ERROR: 'age' is declared but its value is never read.
  return 'Joe';
};

Motivation

It won't necessarily point out a bug in your code, but it will point out unnecessary code, which you can remove and reduce your bundle size.

TypeScript
// noUnusedLocals = ✅true

const getUserName = (): string => {
  return 'Joe';
};

noUnusedParameters

Description

noUnusedParameters does the same thing, but for function parameters instead of local variables. It will emit an error if there are unused parameters in functions.

TypeScript
// noUnusedParameters = ❌false

const getUserName = (age: number): string => {
  return 'Joe';
};
TypeScript
// noUnusedParameters = ✅true

const getUserName = (age: number): string => {
  //=> 💡 COMPILER ERROR: 'age' is declared but its value is never read.
  return 'Joe';
};

Motivation

The motivation is the same, remove unnecessary code.

TypeScript
// noUnusedParameters = ✅true

const getUserName = (): string => {
  return 'Joe';
};

👉 A tip here: if you really want to declare some unused function parameter, you can do so by prefixing it with an underscore _. In our example, we could change age to _age and TypeScript would be happy.

TypeScript
// noUnusedParameters = ✅true

const getUserName = (age: number): string => {
  //=> 💡 COMPILER ERROR: 'age' is declared but its value is never read.
  return 'Joe';
};
TypeScript
// noUnusedParameters = ✅true

const getUserName = (_age: number): void => {};

allowUnusedLabels = false

Description

JavaScript is a multi-paradigm programming language, it's imperative, functional, and object-oriented. Being an imperative language, it supports labels, which are kinda like checkpoints in your code that you can jump to.

TypeScript
// allowUnusedLabels = ✅true

let str = '';

label: for (let i = 0; i < 5; i++) {
  if (i === 1) {
    continue label;
  }
  str = str + i;
}

console.log(str);
//=> 🔉 LOG: "0234"

Labels are very rarely used in JavaScript, and the syntax to declare a label is very close to the syntax to declare a literal object. So most of the time, developers accidentally declare a label thinking that they're creating a literal object.

TypeScript
// allowUnusedLabels = ✅true

const isUserAgeValid = (age: number) => {
  if (age > 20) {
    valid: true;
  }
};

Motivation

To prevent that, we can set allowUnusedLabels to false, which will raise compiler errors if we have unused labels.

TypeScript
// allowUnusedLabels = ❌false

const isUserAgeValid = (age: number) => {
  if (age > 20) {
    valid: true;
    //=> 💡 COMPILER ERROR: Unused label
  }
};

allowUnreachableCode = false

Description

Another thing we can safely get rid of is unreachable code. If it's never going to be executed, there's no reason to keep it. Code that comes after a return statement, for example, is unreachable.

TypeScript
// allowUnreachableCode = ✅true

const fn = (n: number): boolean => {
  if (n > 5) {
    return true;
  } else {
    return false;
  }

  return true; // Unreachable
};

Motivation

By setting allowUnreachableCode to false, the TypeScript compiler will raise an error if we have unreachable code.

TypeScript
// allowUnreachableCode = ❌false

const fn = (n: number): boolean => {
  if (n > 5) {
    return true;
  } else {
    return false;
  }

  return true;
  //=> 💡 COMPILER ERROR: Unreachable code detected.
};

LinkNo Implicit Code

The next category is "no implicit code". There are only 2 flags here:

  1. noImplicitOverride
  2. noImplicitReturns

👉By the way, noImplicitAny and noImplicitThis could also belong here, but they're already in the strict mode category.

noImplicitOverride

Description

If you use a lot of object inheritance, first: WHY? Second: TypeScript 4.3 introduced the override keyword, this keyword is meant to make your member overrides safer.

Motivation

Back to our User and Admin analogy. Let's say that User has a method called greet and you override that method in your Admin class.

TypeScript
class User {
  greet() {
    return 'I am an user';
  }
}

class Admin extends User {
  greet() {
    return 'I am not your regular user';
  }
}

All good and well, until User decides to rename greet to saySomething. Then your Admin class will be out-of-sync.

TypeScript
class User {
  saySomething() {
    return 'I am an user';
  }
}

class Admin extends User {
  greet() {
    return 'I am not your regular user';
  }
}

But fear not, because, with the override keyword, TypeScript will raise a compiler error, letting you know that you're trying to override a method that is not declared in the base User class.

TypeScript
class User {
  saySomething() {
    return 'I am a user';
  }
}

class Admin extends User {
  override greet() {
    //=> ⚠️ COMPILER ERROR: This member cannot have an 'override' modifier because it is not declared in the base class 'User'.
    return 'I am not your regular user';
  }
}

Along with the override keyword, we got the noImplicitOverride flag. Which requires all overridden members to use the override keyword.

TypeScript
// noImplicitOverride = ❌false
class User {
  saySomething() {
    return 'I am a user';
  }
}

class Admin extends User {
  saySomething() {
    return 'I am not your regular user';
  }
}
TypeScript
// noImplicitOverride = ✅true
class User {
  saySomething() {
    return 'I am a user';
  }
}

class Admin extends User {
  saySomething() {
    //=> ⚠️ COMPILER ERROR: This member must have an 'override' modifier because it overrides a member in the base class 'User'.
    return 'I am not your regular user';
  }
}

Besides making your overrides safer, this has the added benefit of making them explicit.

TypeScript
// noImplicitOverride = ✅true
class User {
  saySomething() {
    return 'I am a user';
  }
}

class Admin extends User {
  override saySomething() {
    return 'I am not your regular user';
  }
}

noImplicitReturns

Description

Talking about explicit, we also have the ​noImplicitReturns​ flag, which will check all code paths in a function to ensure that they return a value.

Motivation

For example, let's say you have a function getNameByUserID that receives the ID of a user and returns his name. If the ID is 1, we return Bob and if it's 2, we return Will.

TypeScript
// noImplicitReturns = ❌false
const getNameByUserID = (id: number) => {
  if (id === 1) return 'Bob';
  if (id === 2) return 'Will';
};

There's an obvious issue here. What would happen if we requested the name of a user with ID 3?

Enabling ​noImplicitReturns​ would give us a compiler error saying that not all code paths return a value.

TypeScript
// noImplicitReturns = ✅true
const getNameByUserID = (id: number) => {
  //=> ❌ COMPILER ERROR: Not all code paths return a value.
  if (id === 1) return 'Bob';
  if (id === 2) return 'Will';
};

To deal with that, we have two alternatives:

1 - Deal with cases where ID is neither 1 nor 2. For example, we could return Joe by default.

TypeScript
// noImplicitReturns = ✅true
// ✅ Solution 1
const getNameByUserID = (id: number) => {
  if (id === 1) return 'Bob';
  if (id === 2) return 'Will';
  return 'Joe';
};

2 - We can maintain our implementation as it is, and explicitly type our function return as string | void.

TypeScript
// noImplicitReturns = ✅true
// ✅ Solution 2
const getNameByUserID = (id: number): string | void => {
  if (id === 1) return 'Bob';
  if (id === 2) return 'Will';
};

That goes back to the strictNullChecks motivation. We need to deal with all the possible cases. This flag should be always on.

LinkOthers

The last category is "others". Basically, every useful flag that I couldn't fit into the previous categories. There are 5 flags here:

  1. noUncheckedIndexedAccess
  2. noPropertyAccessFromIndexSignature
  3. noFallthroughCasesInSwitch
  4. exactOptionalPropertyTypes
  5. forceConsistentCasingInFileNames

noUncheckedIndexedAccess

Description

Let's start with noUncheckedIndexedAccess. TypeScript allows us to use index signatures to describe objects which have unknown keys but known values. For example, if you have an object that maps IDs to users, you can use an index signature to describe that.

TypeScript
// noUncheckedIndexedAccess = ❌false
interface User {
  id: string;
  name: string;
}

type UsersByID = {
  [userID: string]: User;
};

declare const usersMap: UsersByID;

With that in place, we can now access any properties of usersMap and get a User in return.

TypeScript
// noUncheckedIndexedAccess = ❌false
interface User {
  id: string;
  name: string;
}

type UsersByID = {
  [userID: string]: User;
};

declare const usersMap: UsersByID;

const example = usersMap.example;
//=> example: User

Motivation

But that's obviously wrong. Not every property in our usersMap will be populated. So usersMap.example should not be of type User, it should be of type User | undefined. That seems like a perfect job for strictNullChecks, but it's not. To add undefined while accessing index properties, we need the noUncheckedIndexedAccess flag.

TypeScript
// noUncheckedIndexedAccess = ✅true
interface User {
  id: string;
  name: string;
}

type UsersByID = {
  [userID: string]: User;
};

declare const usersMap: UsersByID;

const example = usersMap.example;
//=> example: User | undefined

noPropertyAccessFromIndexSignature

Description

On the topic of index properties, we also have the noPropertyAccessFromIndexSignature flag, which forces us to use bracket notation to access unknown fields.

TypeScript
// noPropertyAccessFromIndexSignature = ❌false
interface GameSettings {
  // Known up-front properties
  speed: 'fast' | 'medium' | 'slow';
  quality: 'high' | 'low';
  [key: string]: string;
}

declare const settings: GameSettings;

settings.speed;
settings.quality;
settings.username;
settings['username'];
TypeScript
// noPropertyAccessFromIndexSignature = ✅true

interface GameSettings {
  // Known up-front properties
  speed: 'fast' | 'medium' | 'slow';
  quality: 'high' | 'low';
  [key: string]: string;
}

declare const settings: GameSettings;

settings.speed;
settings.quality;
settings.username;
//=> ⚠️ COMPILER ERROR: Property 'username' comes from an index signature, so it must be accessed with ['username'].
settings['username'];

Motivation

This is very much a linting flag. The benefit here is consistency. We can create the mental model that accessing properties with dot notation signals a certainty that the property exists, while using bracket notation signals uncertainty.

TypeScript
// Certainty
object.prop;

// Uncertainty
object['prop'];

noFallthroughCasesInSwitch

Description

Another very useful flag is noFallthroughCasesInSwitch. If we declare a switch case without break or return statements, our code will run the statements of that case, as well as the statements of any cases following the matching case, until it reaches a break, a return or the end of the switch statement.

TypeScript
// noFallthroughCasesInSwitch = ❌ false

const evenOrOdd = (value: number): void => {
  switch (value % 2) {
    case 0:
      console.log('Even');
    case 1:
      console.log('Odd');
      break;
  }
};

evenOrOdd(2);
//=> 🔉 LOG: "Even"
//=> 🔉 LOG: "Odd"

evenOrOdd(1);
//=> 🔉 LOG: "Odd"

Even if you're a senior developer, it's just too easy to accidentally forget a break statement.

Motivation

With noFallthroughCasesInSwitch enabled, TypeScript will emit compiler errors for any non-empty switch cases that don't have a break or a return statement. Protecting us from accidental fallthrough case bugs.

TypeScript
const a: number = 6;

switch (a) {
  case 0:
    // Error: Fallthrough case in switch.
    console.log('even');
  case 1:
    console.log('odd');
    break;
}

forceConsistentCasingInFileNames

Description

Another easy mistake is to rely on case-insensitive file names.

For example, if your operating system doesn't differentiate lowercase and uppercase characters in file names, you can access a file called User.ts by typing it with a capital "U" or with everything lowercase, it'll work the same way. But it might not work for the rest of your team.

TypeScript
export interface User {
  name: string;
  email: string;
}
TypeScript
// forceConsistentCasingInFileNames = ❌false

import { User } from './User';
// ✅ Case-insensitive operating systems
// ✅ Case-sensitive operating systems

import { User } from './user';
// ✅ Case-insensitive operating systems
// ❌ Case-sensitive operating systems (the filename is User.ts)

By default, TypeScript follows the case-sensitivity rules of the file system it’s running on. But we can change that by enabling the forceConsistentCasingInFileNames flag.

Motivation

When this option is set, TypeScript will raise compilation errors if your code tries to access a file without exactly matching the file name.

We get consistency and avoid errors with case-sensitive operating systems.

TypeScript
// forceConsistentCasingInFileNames = ✅true

import { User } from './User';
// ✅ Case-insensitive operating systems
// ✅ Case-sensitive operating systems

import { User } from './user';
//=> ⚠️ COMPILER ERROR: File name 'user.ts' differs from already included file name 'User.ts' only in casing.
// ✅ Case-insensitive operating systems
// ❌ Case-sensitive operating systems (the filename is User.ts)

exactOptionalPropertyTypes

Description

The last flag I'd like to mention is ​​exactOptionalPropertyTypes. In JavaScript, if you have an object and try to access a property that doesn't exist in it, you get undefined. That's because that property was not defined.

For example, declare an empty object called test and try to see if property exists in test. We'll get false.

TypeScript
const test = {};
'property' in test; //=> false

Ok, we get that. But we can also explicitly define a property as undefined, and that's a little different because now, if we try to see if property exists in test, we'll get true.

TypeScript
const test = { property: undefined };
'property' in test; //=> true

That's all to say that there's a difference between a property being undefined because it wasn't defined and it being undefined because we set it to undefined.

By default, TypeScript ignores that difference, but we can change that behavior by enabling exactOptionalPropertyTypes.

TypeScript
// exactOptionalPropertyTypes = ❌false

interface Test {
  property?: string;
}

const test1: Test = {};
'property' in test1; //=> false

const test2: Test = { property: undefined };
'property' in test2; //=> true

Motivation

With this flag enabled, TypeScript becomes aware of those two different ways of having an undefined property.

TypeScript
// exactOptionalPropertyTypes = ✅true

interface Test {
  property?: string;
}

const test1: Test = {};
'property' in test1; //=> false

const test2: Test = { property: undefined };
//=> ⚠️ COMPILER ERROR: Type '{ property: undefined; }' is not assignable to type 'Test' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
'property' in test2; //=> true

When we use the optional property operator ?, we indicate that a property might be undefined by not being defined. But that won't allow us to explicitly set that property to undefined.

If we want to explicitly define a property as undefined, we'll need to say that it can be undefined.

TypeScript
// exactOptionalPropertyTypes = ✅true

interface Test {
  property?: string | undefined;
}

const test1: Test = {};
'property' in test1; //=> false

const test2: Test = { property: undefined };
'property' in test2; //=> true

LinkConclusion

That's all. If you want to dive deeper into TypeScript, I have a series about TypeScript narrowing. You can watch the full series, for free, on my channel. As always, references are in the description. If you enjoyed the content, you know what to do. And if your company is looking for remote web developers, consider contacting me and my team on lucaspaganini.com. Leave a like, have a great day, and I’ll see you soon.

TypeScript Narrowing Series

LinkReferences

  1. TypeScript strict flag TypeScript Docs
  2. TypeScript CLI compiler TypeScript Docs
  3. JavaScript Strict Mode MDN
  4. ECMAScript Strict Mode Specification ECMAScript Specification
  5. How TypeScript's Strict Mode Actually Fixes TypeScript Eran Shabi (@eranshabi on Twitter)
  6. Why are function parameters bivariant in TypeScript? TypeScript Official FAQ
  7. Cheat Codes for Contravariance and Covariance Matt Handler at Originate
  8. Covariance vs Contravariance in Programming Languages Code Radiance
  9. JavaScript Paradigms MDN
  10. JavaScript Labels MDN
  11. override and the --noImplicitOverride Flag TypeScript 4.3 Release Notes
  12. JavaScript Property Accessors - Dot and Bracket Notations MDN
  13. JavaScript Switch MDN

Join our Newsletter and be the first to know when I launch a course, post a video or write an article.

This field is required
This field is required