20 TypeScript Flags You Should be Using
Do this if you use TypeScript!
 Introduction
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. 
 Dealing 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.
 Our Compiler Options Recommendation
Without further ado, these are the 20 TypeScript compiler options that we recommend:
{
  "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
  }
}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:
- Strict mode
 - No unused code
 - No implicit code
 - Others
 
 Staying 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 
 Strict 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:
strictalwaysStrictnoImplicitAnynoImplicitThisstrictNullChecksstrictBindCallApplystrictFunctionTypesstrictPropertyInitializationuseUnknownInCatchVariables
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.
{
  "compilerOptions": {
    "strict": true,
  }
}TypeScript{
  "compilerOptions": {
    "strict": true,
  }
}{
  "compilerOptions": {
    "alwaysStrict": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "strictBindCallApply": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "useUnknownInCatchVariables": 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.
{
  "compilerOptions": {
    "alwaysStrict": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "strictBindCallApply": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "useUnknownInCatchVariables": true,
    "someFlagFromTheFuture": true,
    "someOtherFlagFromTheFuture": true,
    "yetAnotherFlagFromTheFuture": true,
  }
}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.
'use strict';
var str = 'Hello World';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.
'use strict';
mistypeVariable = 17;
//=> ⚠️ RUNTIME ERROR: mistypeVariable is not definedJavaScript'use strict';
mistypeVariable = 17;
//=> ⚠️ RUNTIME ERROR: mistypeVariable is not defined// 'use strict';
mistypeVariable = 17;
//=> ✅ OK, setting window.mistypeVariable to 17JavaScript// 'use strict';
mistypeVariable = 17;
//=> ✅ OK, setting window.mistypeVariable to 17The 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.
mistypeVariable = 17;
//=> ❌ COMPILER ERROR: Cannot find name 'mistypeVariable'TypeScriptmistypeVariable = 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.
function explicit(explicitAny: any) {}
//=> ✅ OK: We are explicitly setting it to 'any'
function implicit(implicitAny) {}
//=> ❌ COMPILER ERROR: Parameter 'implicitAny' implicitly has an 'any' typeTypeScriptfunction explicit(explicitAny: any) {}
//=> ✅ OK: We are explicitly setting it to 'any'
function implicit(implicitAny) {}
//=> ❌ COMPILER ERROR: Parameter 'implicitAny' implicitly has an 'any' typeMotivation
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.
// noImplicityAny: false ❌
function splitByDots(value) {
  //=> value: any
  //=> returns: any
  return value.split('.');
}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.
// 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?"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.
// 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 😔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.
// 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) {
  //=> ❌ 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(...)// 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'?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.
// noImplicityAny: true ✅
function splitByDots(value: any) {
  //=> value: any
  //=> returns: any
  return value.split('.');
}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:
class User {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  getName() {
    return function () {
      return this.name;
    };
  }
}
const user = new User('Tom');
user.getName()();TypeScriptclass 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.
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 undefinedTypeScriptclass 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 undefinedTo 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.
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()();TypeScriptclass 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?
const users = [{ name: 'Bob', age: 13 }];
const user = users.find((u) => u.age < 10);
console.log(user.name);
//=> ❌ COMPILER ERROR: Object is possibly 'undefined'.TypeScriptconst 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.
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'.TypeScriptconst 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:
interface User {
  name: string;
}
interface Admin extends User {
  permissions: Array<string>;
}
declare let admin: Admin;
declare let user: User;TypeScriptinterface 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.
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; // ❌ ErrorTypeScriptinterface 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; // ❌ ErrorAn 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.
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)TypeScriptinterface 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.
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)TypeScriptinterface 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.
// ❌ Wrong
class User {
  name: string;
  //=> ❌ COMPILER ERROR: Property 'name' has no initializer and is not definitely assigned in the constructor.
  constructor() {}
}TypeScript// ❌ Wrong
class User {
  name: string;
  //=> ❌ COMPILER ERROR: Property 'name' has no initializer and is not definitely assigned in the constructor.
  constructor() {}
}// ✅ Solution 1
class User {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}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.
// ✅ Solution 2
class User {
  name: string | undefined;
  constructor() {}
}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.
// useUnknownInCatchVariables = ❌false
try {
  // Some code...
} catch (err) {
  //=> err: any
  console.log(err.message);
}TypeScript// useUnknownInCatchVariables = ❌false
try {
  // Some code...
} catch (err) {
  //=> err: any
  console.log(err.message);
}// 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
  console.log(err.message);
  //=> ❌ COMPILER ERROR: Object is of type 'unknown'
}// useUnknownInCatchVariables = ✅true
try {
  // Some code...
} catch (err) {
  //=> err: unknown
  if (err instanceof Error) {
    console.log(err.message); //=> err: Error
  }
}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.
// useUnknownInCatchVariables = ❌false
try {
  throw Error('error message');
} catch (err) {
  console.log(err.message);
  //=> LOG: 'error message'
}TypeScript// useUnknownInCatchVariables = ❌false
try {
  throw Error('error message');
} catch (err) {
  console.log(err.message);
  //=> LOG: 'error message'
}// useUnknownInCatchVariables = ❌false
try {
  throw 'error message';
} catch (err) {
  console.log(err.message);
  //=> LOG: undefined
}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.
// 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
  console.log(err.message);
  //=> ❌ COMPILER ERROR: Object is of type 'unknown'
}// 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
  }
}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
  }
} No Unused Code Flags
Now we'll get into the no unused code flags. There are 4 in this category:
noUnusedLocalsnoUnusedParametersallowUnusedLabels = falseallowUnreachableCode = 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.
// noUnusedLocals = ❌false
const getUserName = (): string => {
  const age = 23;
  return 'Joe';
};TypeScript// noUnusedLocals = ❌false
const getUserName = (): string => {
  const age = 23;
  return 'Joe';
};// noUnusedLocals = ✅true
const getUserName = (): string => {
  const age = 23;
  //=> 💡 COMPILATION ERROR: 'age' is declared but its value is never read.
  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.
// noUnusedLocals = ✅true
const getUserName = (): string => {
  return 'Joe';
};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.
// noUnusedParameters = ❌false
const getUserName = (age: number): string => {
  return 'Joe';
};TypeScript// noUnusedParameters = ❌false
const getUserName = (age: number): string => {
  return 'Joe';
};// 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): string => {
  //=> 💡 COMPILER ERROR: 'age' is declared but its value is never read.
  return 'Joe';
};Motivation
The motivation is the same, remove unnecessary code.
// noUnusedParameters = ✅true
const getUserName = (): string => {
  return 'Joe';
};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.
// 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): string => {
  //=> 💡 COMPILER ERROR: 'age' is declared but its value is never read.
  return 'Joe';
};// noUnusedParameters = ✅true
const getUserName = (_age: number): void => {};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.
// 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"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.
// allowUnusedLabels = ✅true
const isUserAgeValid = (age: number) => {
  if (age > 20) {
    valid: true;
  }
};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.
// allowUnusedLabels = ❌false
const isUserAgeValid = (age: number) => {
  if (age > 20) {
    valid: true;
    //=> 💡 COMPILER ERROR: Unused label
  }
};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.
// allowUnreachableCode = ✅true
const fn = (n: number): boolean => {
  if (n > 5) {
    return true;
  } else {
    return false;
  }
  return true; // 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.
// allowUnreachableCode = ❌false
const fn = (n: number): boolean => {
  if (n > 5) {
    return true;
  } else {
    return false;
  }
  return true;
  //=> 💡 COMPILER ERROR: Unreachable code detected.
};TypeScript// allowUnreachableCode = ❌false
const fn = (n: number): boolean => {
  if (n > 5) {
    return true;
  } else {
    return false;
  }
  return true;
  //=> 💡 COMPILER ERROR: Unreachable code detected.
}; No Implicit Code
The next category is "no implicit code". There are only 2 flags here:
noImplicitOverridenoImplicitReturns
👉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.
class User {
  greet() {
    return 'I am an user';
  }
}
class Admin extends User {
  greet() {
    return 'I am not your regular user';
  }
}TypeScriptclass 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.
class User {
  saySomething() {
    return 'I am an user';
  }
}
class Admin extends User {
  greet() {
    return 'I am not your regular user';
  }
}TypeScriptclass 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.
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';
  }
}TypeScriptclass 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.
// noImplicitOverride = ❌false
class User {
  saySomething() {
    return 'I am a user';
  }
}
class Admin extends User {
  saySomething() {
    return 'I am not your regular user';
  }
}TypeScript// noImplicitOverride = ❌false
class User {
  saySomething() {
    return 'I am a user';
  }
}
class Admin extends User {
  saySomething() {
    return 'I am not your regular user';
  }
}// 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';
  }
}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.
// noImplicitOverride = ✅true
class User {
  saySomething() {
    return 'I am a user';
  }
}
class Admin extends User {
  override saySomething() {
    return 'I am not your regular user';
  }
}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.
// noImplicitReturns = ❌false
const getNameByUserID = (id: number) => {
  if (id === 1) return 'Bob';
  if (id === 2) 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.
// 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';
};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.
// noImplicitReturns = ✅true
// ✅ Solution 1
const getNameByUserID = (id: number) => {
  if (id === 1) return 'Bob';
  if (id === 2) return 'Will';
  return 'Joe';
};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.
// noImplicitReturns = ✅true
// ✅ Solution 2
const getNameByUserID = (id: number): string | void => {
  if (id === 1) return 'Bob';
  if (id === 2) return 'Will';
};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.
 Others
The last category is "others". Basically, every useful flag that I couldn't fit into the previous categories. There are 5 flags here:
noUncheckedIndexedAccessnoPropertyAccessFromIndexSignaturenoFallthroughCasesInSwitchexactOptionalPropertyTypesforceConsistentCasingInFileNames
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.
// noUncheckedIndexedAccess = ❌false
interface User {
  id: string;
  name: string;
}
type UsersByID = {
  [userID: string]: User;
};
declare const usersMap: UsersByID;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.
// noUncheckedIndexedAccess = ❌false
interface User {
  id: string;
  name: string;
}
type UsersByID = {
  [userID: string]: User;
};
declare const usersMap: UsersByID;
const example = usersMap.example;
//=> example: UserTypeScript// noUncheckedIndexedAccess = ❌false
interface User {
  id: string;
  name: string;
}
type UsersByID = {
  [userID: string]: User;
};
declare const usersMap: UsersByID;
const example = usersMap.example;
//=> example: UserMotivation
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.
// noUncheckedIndexedAccess = ✅true
interface User {
  id: string;
  name: string;
}
type UsersByID = {
  [userID: string]: User;
};
declare const usersMap: UsersByID;
const example = usersMap.example;
//=> example: User | undefinedTypeScript// noUncheckedIndexedAccess = ✅true
interface User {
  id: string;
  name: string;
}
type UsersByID = {
  [userID: string]: User;
};
declare const usersMap: UsersByID;
const example = usersMap.example;
//=> example: User | undefinednoPropertyAccessFromIndexSignature
Description
On the topic of index properties, we also have the noPropertyAccessFromIndexSignature flag, which forces us to use bracket notation to access unknown fields.
// 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 = ❌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'];// 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'];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.
// Certainty
object.prop;
// Uncertainty
object['prop'];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.
// 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"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.
const a: number = 6;
switch (a) {
  case 0:
    // Error: Fallthrough case in switch.
    console.log('even');
  case 1:
    console.log('odd');
    break;
}TypeScriptconst 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.
export interface User {
  name: string;
  email: string;
}TypeScriptexport interface User {
  name: string;
  email: string;
}// 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)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.
// 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)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.
const test = {};
'property' in test; //=> falseTypeScriptconst test = {};
'property' in test; //=> falseOk, 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.
const test = { property: undefined };
'property' in test; //=> trueTypeScriptconst test = { property: undefined };
'property' in test; //=> trueThat'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.
// exactOptionalPropertyTypes = ❌false
interface Test {
  property?: string;
}
const test1: Test = {};
'property' in test1; //=> false
const test2: Test = { property: undefined };
'property' in test2; //=> trueTypeScript// exactOptionalPropertyTypes = ❌false
interface Test {
  property?: string;
}
const test1: Test = {};
'property' in test1; //=> false
const test2: Test = { property: undefined };
'property' in test2; //=> trueMotivation
With this flag enabled, TypeScript becomes aware of those two different ways of having an undefined property.
// 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; //=> trueTypeScript// 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; //=> trueWhen 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.
// exactOptionalPropertyTypes = ✅true
interface Test {
  property?: string | undefined;
}
const test1: Test = {};
'property' in test1; //=> false
const test2: Test = { property: undefined };
'property' in test2; //=> trueTypeScript// exactOptionalPropertyTypes = ✅true
interface Test {
  property?: string | undefined;
}
const test1: Test = {};
'property' in test1; //=> false
const test2: Test = { property: undefined };
'property' in test2; //=> true Conclusion
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.
 Related
 References
- TypeScript strict flag TypeScript Docs
 - TypeScript CLI compiler TypeScript Docs
 - JavaScript Strict Mode MDN
 - ECMAScript Strict Mode Specification ECMAScript Specification
 - How TypeScript's Strict Mode Actually Fixes TypeScript Eran Shabi (@eranshabi on Twitter)
 - Why are function parameters bivariant in TypeScript? TypeScript Official FAQ
 - Cheat Codes for Contravariance and Covariance Matt Handler at Originate
 - Covariance vs Contravariance in Programming Languages Code Radiance
 - JavaScript Paradigms MDN
 - JavaScript Labels MDN
 overrideand the--noImplicitOverrideFlag TypeScript 4.3 Release Notes- JavaScript Property Accessors - Dot and Bracket Notations MDN
 - JavaScript Switch MDN