My Relationship With the ! (bang) Operator in Typescript

Danny Vernovsky
AT&T Israel Tech Blog
4 min readJul 26, 2023

--

https://powerlisting.fandom.com/wiki/Collision_Inducement

One of the wonderful things about Typescript is that it allows developers to feel safe. Seriously. When I write code with Typescript and I feel that it got my back.

For example, I can write this:

class Person {
address: Address;
}
const person = new Person();
console.log(person.name);
// Property 'name' does not exist on type 'Person'.

This small code sample that all who write Typescript take for granted, is the simplest proof of why Typescript is awesome. It allows developers to move a lot of common errors from runtime to compile time. But I know you know all of this already 😁

That’s not the whole story though. You see, by default, Typescript is very forgiving, and rightfully so. By being forgiving it allows developers to gradually switch from Javascript to Typescript.

When we start a new project, however, we’d like Typescript to be very strict. We want to use its power to keep us safe. In order to make it less forgiving we need to set strict: true in the tsconfig.json file. But, once you do that Typescript becomes so strict you cannot write one line of code without it complaining. One of the most frustrating complaints is that the code above will not compile for many reasons.

Let’s look at the code again with strict mode:

class Person {
address: Address;
}
const person = new Person();
console.log(person.name);
// Property 'address' has no initializer and is not definitely assigned in the constructor

Now this code fails even sooner. It forces us to initialize the address property. Class property (public or private) can have two states, required or optional. Let’s look at required first. If it is required, in strict mode we must initialize it either when it is declared (default value) or via a constructor. If we want to initialize it via a constructor, with large classes we could ask for all of the properties in a constructor, but with so many properties this can look very bad.

And this is where we start to improvise.

To solve this I sometimes see programmers use the “!” (bang operator) trick at the end of the variable. When used at the end of the statement this operator is known as “definite assignment assertion” or in short the “bang” operator. It is used to tell Typescript compiler that there will be a value here eventually. So the code becomes something like this:

class Person {
address!: Address;
name!: string;
}
const person = new Person();
console.log(person.name);

That’s good and all but, if we will think about it, what we’ve actually said to the compiler is: “Believe me, I’m an engineer! Trust me because everything will be fine”. And this code will work, name will be undefined but the code will work. But what about this example:

class Person {
address!: Address;
name!: string;
}
const person = new Person();
console.log(person.address.street);

This code will compile but will fail in runtime. It will fail on something that we tried to fix by embracing Typescript in the first place. But, hey… Does this mean we cannot use ! anywhere?

Well… there are some places we can use ! safely. Typescript is good for interpreting types. The “bang” operator has another kind where it is called the “non-null assertion operator”

Let’s look at another example:

function printName(name?: string){
}

This function receives an optional parameter that gets the type string | undefined. So in order to make it a concrete type we need to write the following code:

function printName(name?: string){
if(name){
print(name);
}
}

Inside the if statement Typescript will interpret the variable as a concrete string which is actually another example of how awesome Typescript is. But let’s look at another example:

function printName(id: string){
if(usersMap.has(id)){
const user = usersMap.get(id);
print(user.name);
}
}

This example will not compile. Although we know as a fact that this code will always work, Typescript cannot connect the .has function with the subsequent.get on the same key. So for this example, we can write something like:

function printName(id: string){
if(usersMap.has(id)){
const user = usersMap.get(id)!;
print(user.name);
}
}

By adding ! after get we explicitly say for Typescript to trust us and for a good reason this time. That being said, we can just write the code a little differently and solve the issue this way as well:

function printName(id: string){
const user = usersMap.get(id);
if(user){
print(user.name);
}
}

Wrapping up

My feelings about ! are mixed, I do understand that we might need it sometimes but I just mostly see it being used incorrectly, so generally I try to stay away.

In order to fight it we can use the relevant eslint rule described here: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/no-non-null-assertion.md

And finally just a general thought.

There are more features in Typescript I’m trying not to use. Why are they in the language you might ask? Mostly there are reasons. But the fact that some features exist doesn’t automatically mean you have to use them.

Resources

--

--