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:
strict
alwaysStrict
noImplicitAny
noImplicitThis
strictNullChecks
strictBindCallApply
strictFunctionTypes
strictPropertyInitialization
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.
{
"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 defined
JavaScript'use strict';
mistypeVariable = 17;
//=> ⚠️ RUNTIME ERROR: mistypeVariable is not defined
// 'use strict';
mistypeVariable = 17;
//=> ✅ OK, setting window.mistypeVariable to 17
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.
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' type
TypeScriptfunction 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.
// 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 undefined
TypeScriptclass 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.
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; // ❌ Error
TypeScriptinterface 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
.
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:
noUnusedLocals
noUnusedParameters
allowUnusedLabels = false
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.
// 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:
noImplicitOverride
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.
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:
noUncheckedIndexedAccess
noPropertyAccessFromIndexSignature
noFallthroughCasesInSwitch
exactOptionalPropertyTypes
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.
// 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: User
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.
// noUncheckedIndexedAccess = ✅true
interface User {
id: string;
name: string;
}
type UsersByID = {
[userID: string]: User;
};
declare const usersMap: UsersByID;
const example = usersMap.example;
//=> example: User | undefined
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.
// 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; //=> false
TypeScriptconst 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
.
const test = { property: undefined };
'property' in test; //=> true
TypeScriptconst 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
.
// exactOptionalPropertyTypes = ❌false
interface Test {
property?: string;
}
const test1: Test = {};
'property' in test1; //=> false
const test2: Test = { property: undefined };
'property' in test2; //=> true
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.
// 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
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
.
// exactOptionalPropertyTypes = ✅true
interface Test {
property?: string | undefined;
}
const test1: Test = {};
'property' in test1; //=> false
const test2: Test = { property: undefined };
'property' in test2; //=> true
TypeScript// 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
override
and the--noImplicitOverride
Flag TypeScript 4.3 Release Notes- JavaScript Property Accessors - Dot and Bracket Notations MDN
- JavaScript Switch MDN