A note on TypeScript non-null assertion operator

Tar Viturawong
6 min readMar 21, 2019

--

Riddle: What is the type of the constant named “y”? (answer at the bottom)

Today, as I was browsing React’s typedoc on component context, I came across something new about something that’s been in TypeScript for a while (way back in the brave new world of 2.0, in fact), namely the non-null assertion operator.

If using the new style context, re-declare this in your class to be theReact.ContextType of your static contextType.* static contextType = MyContext
* context!: React.ContextType<typeof MyContext>
*
@deprecated — if used without a type annotation, or without static contextType

“context!”? … You scared me!

It turned out that this operator ! placed after an expression is TypeScript’s non-null assertion operator. It lets you deliberately ignore an expression’s possible null-ness on a case-by-case basis.

let a: { foo: number }|null = null;
a.foo // ERROR: a is possibly null
a!.foo // OK, type number

You can also use this operator to deliberately ignore void-ness, both owing to deliberate assignment and owing to the value not being reliably detected by TypeScript to have been assigned.

let a: { foo: number };
new Promise(() => a = { foo : 1 });
a.foo // ERROR: a is used before assigned (wrong*)
a!.foo // OK

*A Promise callback is synchronously called on instantiating the promise, so a is expected to be assigned. So this TypeScript error is nonsense. But it’s understandable that TypeScript cannot discriminate which expression is called immediately.

And this works just as well:

let a: { foo: number } | undefined = void 0;
a.foo // ERROR: a is possibly undefined
a!.foo // OK

On class declaration

As shown in the quoted typedoc on React’s context, you can also put! after a property declaration inside a class, and TypeScript will give up type-checking that property for not being initialized properly. For instance, with this React callback ref (inlined for brevity).

class MyClass extends react.Component {
private el!: HTMLInputElement;
render(){
return <input ref={ el => this.el = el! }/>
}
componentDidMount(){
this.el.focus();
}
}

Without ! on the property declaration we will get a complaint that we never initialized el which is OK because we know that React will supply it with a proper value before we use it anyway. For part of the component’s lifecycle where it matters, el will always have a value, so we can also safely ignore the time where React assigns null to the ref by putting ! similarly in the ref callback.

Splitting hair over nothing

There are two subtle things to note:

  • This operator will not ignore falsiness, so it will not ignore a union with false.
  • The operator curiously also does not work on a union with void. This seems to be another subtle difference between the void and undefined types.

Ignore responsibly

The fine print of this operator is that it is completely your responsibility to take the risk of ignoring the nullness of your expression. In fact, TypeScript’s release note implicitly hints at a good practice:

// Compiled with --strictNullChecks
function validateEntity(e?: Entity) {
// Throw exception if e is null or invalid entity
}

function processEntity(e?: Entity) {
validateEntity(e);
let s = e!.name; // Assert that e is non-null and access name
}

That us to say, for the sake of your (and your fellow engineers’) sanity, you should probably throw a meaningful exception if it turns out that your assumption is wrong.

When (not) to use it?

TypeScript is wonderful in acknowledging that it sometimes cannot be as smart as we humans are in understanding the domain context in our specific applications. For this reason, there exist types such as any and unknown as well as the ability to globally ignore nullchecks by turning off strictNullChecks inside tsconfig.json. These features of TypeScript make it more humanly reasonable. This one-off nullcheck lift belongs to this throng of human overrides.

However, machines are generally better than humans when it comes to consistency. By overriding TypeScript’s safety checks you (and your peers — this is important!) assume the responsibility of maintaining the knowledge of how the shortcut can be seen as safe. Therefore, I would recommend that at least one of the following holds true when you use the operator:

  • The usage context (e.g. according to knowledge about your code’s ecosystem that can reasonably be expected from all) obviously suggests that the use of the operator is safe. React callback refs of a static element is an example of such a case.
  • The operator use is immediately preceded by a check that would suggest that the usage is safe. The assertion used in TypeScript’s release notes is an example of such a case.
  • There is plenty of documentation why this is safe that is understandable by all. Often, if this is convoluted, it’s better to self-document with an explicit type assertion.

Plus always this one point:

  • It is absolutely clear where the scope of your knowledge/assumption about the safety of ignoring null begins and ends. Ideally, this is as small as possible in terms of code area. Do not allow the requirement for this knowledge to leak across natural code boundaries such as functions or classes (or even worse, files!).

Do not use it just because you are lazy to think through why something is flagged as maybe null by TypeScript when it shouldn’t be. Ad-hoc overrides in this manner often merely hide a deeper flaw in reasoning that may be surfacing in other areas of your code.

Alternatives

In the same way that the a nation’s leader shouldn’t learn of the nuclear button until he or she fully understands the geopolitics and humanitarian challenges of their region, in a way I almost considered it a good fortune that I remained ignorant of this operator for so long.

While I have shown cases where the human override is the best way to reduce verbosity (like the Promise callback example), often there are ways to overcome the nuisance of redundant nullchecks without actually providing a manual override and all of its maintenance obligations. Let’s explore a couple of cases:

I’m not null because something else is different

You have a UI state that represents an AJAX request progress. It’s either in the initial state, pending state, complete state, or error state. Only in the complete state do you have the response, otherwise it is null.

While it is possible that you would make the response type nullable and just ignore the nullness once you have determined that you are in complete state, you might want to (if not constrained by e.g. third-party libraries) consider putting your UI state representation into a discriminated union. I covered discriminated unions at length in an article on singleton types.

That is, instead of this:

type AjaxState<T> = {
state: 'initial'|'pending'|'complete'|'error';
response: T|null;
}
// assume `ajaxState` is `AjaxState<number[]>`
if (ajaxState.state === 'complete'){
// "complete" state means response always exists
console.log(ajaxState.response!.length);
}

Communicate your human intention — that “complete” state means response always exists — to TypeScript.

type AjaxState<T> = 
{ state: 'initial'|'pending'|'error', response: null } |
{ state: 'complete', response: T };
// assume `ajaxState` is `AjaxState<number[]>`
if (ajaxState.state === 'complete'){
console.log(ajaxState.response.length);
}
console.log(ajaxState.response.length);
// above will give ERROR, value is possibly null

Not only it is safe in assertion, it also makes sure that you cannot change your AjaxState state in ways that are inconsistent with the typing. Try it for yourself!

I just know that it’s fine

In my last article I introduced idea that you can use TypeScript generics to create your own arbitrary type logic. In that piece, I mentioned this function:

function requireValue<ValueType>(input: MaybeNull<ValueType>){
if (input === null) throw Error("value is required!");
return input;
}

Where MaybeNull<T> = T|null

This essentially provides a typesafe alternative to the ! operator and a way for you to throw a meaningful, semantic error (just pass your own favourite message instead), and you could use it like this:

// iKnowThisIsntNull is technically null|{ doSomething: () => void }
requireValue(iKnowThisIsntNull).doSomething();

You could even safe the result into a new variable, from which point it is null-free.

Conclusion

The postfix ! operator can provide a quick way to ignore the null part of an expression. As with other TypeScript mechanisms that allow human overrides like any and the as operator, usage comes with a cost of knowledge maintenance. In this article I have outlined the healthy way of using this operator that could also extend to other human overrides.

Now go and enjoy that healthy human-machine relationship, and celebrate our appreciation of each other ❤️.

*Answer to the riddle at the top: The type of y as shown in the expressions in the image is the TypeScript type known as never. Becausex is a constant, its type is the same as its value literal’s type:null . The expression for constant yx! — is saying “take the value of x and ignore the case where it is null”. But since x is always null, being a constant, we have an impossible scenario in y .

--

--

Tar Viturawong

I write dev articles. I code, love, laugh, cry, eat, goof, screw up, celebrate, and wonder.