What is the ! (exclamation mark / bang) operator in TypeScript?

What is the ! (exclamation mark / bang) operator in TypeScript?
Photo by meagan paddock

Sometimes in a TypeScript codebase you might see code like this:

const bar = myObject!.foo!.bar

// or

const obj = myObject!

The exclamation/bang operator that comes after accessing a property (also called a "postfix bang") is known as a non-null assertion.

The use case for this operator is to convert a "nullish" (aka, possibly null or undefined) value type into a non-null equivalent. Here's an example:

function reverse(value: string | null | undefined) {
  // The ! forces `value` to act as a `string` type
  const nonNullValue: string = value!
}

However, as the TypeScript docs explain:

this doesn’t change the runtime behavior of your code, so it’s important to only use ! when you know that the value can’t be null or undefined.

This is a helpful warning! If we look at what the compiled output we can see it's exactly the same as the TypeScript version minus the ! syntax:

const nonNullValue = value;

Even though this made the TypeScript compiler error go away, it's not actually type-safe code. What the postfix bang effectively says is "Hey, TypeScript even though you think this is a type error trust me it's not". This behavior is similar to other type assertion operators like as (eg, const foo = bar as string) which also do not impact runtime behavior.

We should be careful about this "trust me" attitude in our code. We as developers tend to have this assumption that we know better than the compiler. In practice I find we rarely do.

So where does that leave us? Why did TypeScript add a feature that ignores the type checker? Should we ban it entirely?

The rest of this article covers use cases to consider using non-null assertions, alternatives you can use instead, as well as ways to prevent usage in your projects.

Why does the non-null assertion operator exist?

The non-null assertion operator was introduced back in TypeScript 2.0. At the time, TypeScript did not have as robust support for type narrowing. Here's an example:

type Person = {
  name: string
}

function validatePerson(person: Person | undefined): never {
  if (!person) {
    throw new Error('person is not valid!')
  }
}

function processPerson(person: Person | undefined) {
  validatePerson(person)
  
  // typescript thinks person is still `Person | undefined` here
}

The non-null assertion can safely be used in this case since we can be certain we would not make it past validatePerson if person was undefined:

function processPerson(person: Person | undefined) {
  validatePerson(person)
  
  const name: string = person!.name // trust me!
}

The problem is this is a dangerous assumption to make: someone could remove validatePerson, or the internals of validatePerson could change to not throw an error and the person!.name line would still pass the TypeScript compiler.  The alternative at the time was to write your assertions completely inline which can create messy, tangled code for complex type checks.

But it was a workaround to a difficult problem around type narrowing; a problem that TypeScript has since improved on.

What are the alternatives to non-null assertions?

These days there are several options available that provide the same or similar benefit as non-null assertions. Let's take a look.

Optional chaining

In some cases you don't need a dedicated validatePerson assertion function, and can instead take a more permissive approach to data access.

Prior to the addition of optional chaining, you might write something like this:

const name: string | undefined = person ? person.name : undefined

With optional chaining in TypeScript, that can be simplified to:

const name: string | undefined = person?.name

This isn't quite the same behavior as the validatePerson approach in the original example; it generally requires a more permissive programming style where the rest of your application is equipped to handle undefined values or falls back to some reasonable default (ie, person?.name || "").

Optional chaining is one option to consider in place of non-null assertions because they are supported by the TypeScript compiler so you aren't opting out of type checking!

Type predicates

To keep the validatePerson pattern in tact, another approach would be to modify the code slightly:

function validatePerson(person: Person | undefined): person is Person {
    return !!person
}

function processPerson(person: Person | undefined) {
  if (!validatePerson(person)) {
      throw new Error('person is not valid!') 
  }
  
  const name: string = person.name // person is guaranteed to be Person
}

The magic is in the return type of validatePerson: person is Person. This is called a type predicate. Functions that return type predicates must have a boolean return value.

In the above example, if the predicate returns true, it indicates the the input value person is actually type Person. This can then be leveraged to narrow the type of person in the processPerson function.

Since we throw an error inside the if block, then any time person is accessed after the if block it is guaranteed to be of type Person. You see for yourself in this playground example.

Even though this example isn't quite the same organizationally as the original, it's a small modification with big upsides: we get to keep TypeScript's type-checker in play and stop pretending we know better!

Now that we've looked at some alternatives to non-null assertions you might think we never need this feature. But there's at least one use case I still find them useful.

When should non-null assertions be used?

When writing unit tests you want the least amount of control flow possible. Too much complexity in your tests and you end up needing tests for your tests! Here's an example:

// utils.ts
function getPersonInitials(person: Person | undefined): string | undefined {
  if (!person) {
    return undefined
  }
  
  const [firstName, lastName] = person.name.split(' ') || []
  
  return firstName[0] + lastName[0]
}

// utils.spec.ts
it('should return a string with 2 characters', () => {
  const person = {
    name: 'John Smith'
  }
  
  expect(getPersonInitials(person).length).toEqual(2) // Type error!
})

TypeScript will throw an error in the above test. Since getPersonInitials can return undefined, it's unsafe to directly access the length property. We might be tempted to add something like this:

it('should return a string with 2 characters', () => {
  const person = {
    name: 'John Smith'
  }
  
  const initials = getPersonInitials(person)
  
  /* !! DON'T DO THIS !! */
  if (initials) {
      expect(initials.length).toEqual(2) // no more type error!
  }
})

The problem is with conditionally calling expect. If the behavior of this function changes, this test could start passing because expect is never called so nothing is ever tested.

An alternative would be to remove the if block and use non-null assertions:

it('should return a string with 2 characters', () => {
  const person = {
    name: 'John Smith'
  }
  
  const initials = getPersonInitials(person)
  
  expect(initials!.length).toEqual(2) // no more type error!
})```

Tests that fail loudly are easier to debug and fix. If the implementation changes for this function and the test starts failing, the test would provide a message like:

Expected: 2
Received: undefined

The message also includes a reference to the line that failed. This gives you a direct path to fixing the issue with no possibility of expect not being called.

In summary, non-null assertions are useful for places in code that are supposed to fail loud and fail fast. For everything else, use non-null assertions at your own risk!

Can non-null assertions be used in JavaScript?

TypeScript can be thought of "JavaScript with types". In other words, every syntactically valid JavaScript program is also a syntactically valid TypeScript program. However, the reverse is not true.

Non-null assertions are a TypeScript-only feature and as we saw above, these assertions are removed entirely after compilation because they are not valid JavaScript.

Preventing usage of non-null assertions

In my projects, I use typescript-eslint to enforce good coding practices. One of the rules this package provides is called no-non-null-assertion. By default, the rule is enabled for all files. However, with overrides I can allow it for specific scenarios like tests. Here's an example:

// .eslintrc
{
  ...
  "rules": {
    // enabled by default for all files
    "@typescript-eslint/no-non-null-assertion": 2
  },
  "overrides": [
    {
      "files": "*.spec.*",
      "rules": {
        // disabled for test files
        "@typescript-eslint/no-non-null-assertion": 0
      }
    }
  ]
}

Summary

TypeScript offers many useful features to developers that improve the type safety of our programs. But, given the dynamic nature of JavasScript sometimes we need escape hatches to stay productive. Non-null assertions are one such escape hatch. The feature has some use cases where it's helpful, but more commonly it hurts the integrity of your project.