TypeScript is a strongly typed programming language which builds on JavaScript giving you better tooling at any scale. It’s widely used across frontend projects and the NodeJS ecosystem.
The user can customise the configuration to be more or less “strict”. Here less means it relaxes type safety – which will effectively mean that some stuff isn’t checked.
By default, TypeScript is less restrictive but there are many flags to increase type safety – especially the “strict” one which activates the whole set of flags under the hood, like:
- strictFunctionTypes
- strictNullChecks
- strictPropertyInitialization
- noImplicitAny
- noImplicitThis
And more, as the list of those flags varies between TypeScript versions.
Let’s take a look at how some of these affect error catching at compilation time.
noImplicitAny
Enabling this flag means that the TypeScript compiler will throw an error when we have a function in our code which parameters would implicitly take the type any. Thanks to this, we can easily detect such places.
Here is an example code:
// here “a” and “b” parameters are typed implicitly as any // hence enabling the flag raise an error function add(a, b) { return a + b; } // here “c” param is typed implicitly as number // no error raised in this case by the compiler [1,2,3].map(function pow(c) { return c * c; }
noImplicitThis
Enabling this flag will raise an error at any place we reference “this” keyword and the compiler can’t recognise what “this” means so it has implied “any” type.
class Rectangle { constructor(private width: number, private height: number) {} getAreaFunction() { return function () { // 'this' implicitly has type 'any' return this.width * this.height; }; } }
As an example, we can have a “Rectangle” class that has the “getAreaFunction” method that returns a function. That returned function tries to access “this.width” and “this.height” – but there is no context for this inside the function.
The above code example is taken from https://www.typescriptlang.org/tsconfig.
noImplicitReturn
This flag means that if a function has different paths to return code (e.g. it has an “if” or “try/catch” block with “return” inside) then each of those paths must return a value and it cannot be implicit (void). Additionally, if we define the return type of that function then all paths must return that defined type.
declare function dangerousFunction(): string; function doSomething(): string { try { return dangerousFunction(); } catch (error) { console.error(“Ouch, an error!”, error); // implicit return undefined } }
strictPropertyInitialization
This flag implies that when a class has a property declared that property must be initialised or properly typed – allow undefined as a value explicitly.
class Person { private name: string; // error, as it’s not initialized } ``` We can take several approaches in order to fix that error: ``` // #1 allow undefined as a value private name: string | undefined; // #2 move the property to the constructor, // so it’s passed on instance creation time constructor (private name: string) {} // #3 mark field as optional private name?: string; // #4 add not-null-assertion mark (danger solution) // we need to be 100% sure that the value will be there private name!: string;
strictNullChecks
By default in TypeScript, “null” and “undefined” are silently treated as a part of any type defined in the code. That means when we declare a variable with the type number TypeScript treats it as number | null | undefined.
This behaviour can be disabled with the “strictNullChecks” flag – when it’s turned on we need to explicitly mark the type to contain “null” or “undefined” in order to be able to assign one of those.
type Person = { name: string; balance?: number; } declare const people: Person[]; // #1: this raises an error as find can return undefined const joe: Person = people.find(person => person.name === “Joe”); const patrick: Person = { name: “Patrick” }; // #2: this raises an error as balance is optional const balance = patrick.balance.toFixed(0);
strictFunctionTypes
This one is probably the hardest of all described in this article. This flag causes functions parameters to be checked more correctly. A simple example showing this improvement can be like this: we have a function that takes a string as an argument and we try to assign it to a place that is typed as a function that takes a string or a number as an argument.
When this flag is disabled looks like all is good, but TypeScript will throw an error when we enable it.
function greetings(name: string) { console.log("Hello, " + name.toLowerCase()); } type StringOrNumberFunc = (ns: string | number) => void; let func: StringOrNumberFunc = greetings; // ❌ Error
We receive the error when assigning to the “func” variable because the parameter type of “StringOrNumberFunc” is broader than defined in the “greetings” function. “greetings” can handle only the strings, but “StringOrNumberFunc” can receive strings and numbers – so the numbers are not covered by the “greetings” function.
Another example can be shown with higher-order functions (functions that take functions as parameters). Let’s assume that we have the following interfaces that inherit from each other starting from the most generic one: Food – Fruit – Apple.
For each interface, we have a function that can process it. Then we define a higher-order function that will be a processing engine for “Fruits” and we try to pass all different processing functions to it.
interface Food { name: string } interface Fruit extends Food { sweetness: number } interface Apple extends Fruit { color: string } declare let fruit: Fruit; declare function processFood(f: Food): void; declare function processFruit(f: Fruit): void; declare function processApple(a: Apple): void; function processFruitHof( processFn: (d: Fruit) => void, f: Fruit ) { processFn(f); } processFruitHof(processFruit, fruit); // ✔️ OK processFruitHof(processFood, fruit); // ✔️ OK processFruitHof(processApple, fruit); // ❌ Error
When the flag is disabled all seems to be correct, but when it’s enabled the last line causes the error. Only the last one, we can still pass “processFood” to “processFruitHof”. But… Why?
For structure compatibility TypeScript checks covariance, so when operating on objects we can pass subtype to the place where the type is required as the subtype contains all that’s needed by the type plus something more.
On the other hand, for functions it checks function parameters by covariance, so we can only pass a function with a supertype parameter. This is because it is type-safe. Class “Food” is more generic than “Fruit”, so “Food” has fewer methods. In contrast, “Apple” is more specific than “Fruit” and we can do more with it.
In the second invocation of “processFruitHof” we pass “processFood” function which takes “Food” as a parameter. So it takes a more generic object. And when we invoke that function inside “processFruitHof” passing “Fruit” as a parameter everything is good – because “Food” was expected and we gave the “Fruit”. After all, every “Fruit” is “Food” – so the contract is fulfilled.
When we look at the third invocation of “processFruitHof” we pass there “processApple” which expects “Apple” as a parameter. Then inside “processFruitHof” when we try to invoke “processApple” we pass Fruit which is not enough – that’s why we got the error.
noUncheckedIndexAccess
This flag is not included in the strict bundle – you need to enable it yourself if you wish to use it. With this flag, we increase the safety when referencing elements of an array or object properties through square bracket syntax ( object[“X”] ).
When the flag is enabled, any such reference results in typing the extracted element as T | undefined – with which we have to deal in the code, so the downside of this flag is additional overhead, sometimes completely unnecessary (e.g. when we are inside a typical for loop and we are guaranteed to not go beyond the scope of the array). On the other hand, we gain more safety when reaching for elements that may not exist.
declare let people: Person[]; // without the flag the type of first and tenth is Person const first = people[0]; const tenth = people[10]; // when the flag is on the type is Person | undefined declare let peopleMap: Record<string, Person>; // the same happens when reaching for object property const joe = peopleMap["joe"];
Summary
TypeScript is a flexible language that can be configured to be either less or more restrictive. As frontend developers, we have many flags to customise the compilation process to fit our needs, but we should be aware of the gains and losses of each option.
I hope this article has increased your knowledge about strict mode flags and you can now use them more consciously in your current and future projects. We should know our tools… especially if we have as many as in the frontend ecosystem 🙂