Search code examples
javascriptreactjstypescript

Should we use ! (non-null assertion) or ?. (optional chaining) in TypeScript?


I'm learning TypeScript and React, and when I came across this code, I found that either ! (non-null assertion) and ?. (optional chaining) can be used.

import { FC, FormEvent, useRef } from "react";

const NewTodo: FC = () => {
  const textInputRef = useRef<HTMLInputElement>(null);

  function todoSubmitHandler(ev: FormEvent) {
    ev.preventDefault();
    //                               v here v
    const enteredText = textInputRef.current!?.value;
    console.log(enteredText);
  }

  return (
    <form onSubmit={todoSubmitHandler}>
      <div>
        <label htmlFor="todo-text">Todo Text</label>
        <input type="text" id="todo-text" ref={textInputRef} />
      </div>
      <button type="submit">ADD TODO</button>
    </form>
  );
};

export default NewTodo;

What I know about ! is that it tells Typescript that this value is never null nor undefined. On the other hand, ?. is used to prevent errors when no property found and instead returns undefined. In the above example, I can use either ! or ?. or even both of them combined !?., and Typescript compiler does not complain. So which one is best and safest to use?


Solution

  • Optional chaining ?. is safer to use than non-null assertions !.

    Consider the following interface:

    interface Foo {
        bar?: {
            baz: string;
        }
    }
    

    The bar property is optional. If it doesn't exist it will be undefined when you read it. If it does exist it will have a baz property of type string. If you just try to access the baz property without making sure that bar is defined, you'll get a compiler error warning you about a possible runtime error:

    function oops(foo: Foo) {
        console.log(foo.bar.baz.toUpperCase()); // compiler error
        // -------> ~~~~~~~
        // Object is possibly undefined
    }
    

    Optional chaining has actual effects at runtime and short-circuits to an undefined value if the property you're trying to access does not exist. If you don't know for sure that a property exists, optional chaining can protect you from some runtime errors. The TypeScript compiler does not complain with the following code because it knows that what you are doing is now safe:

    function optChain(foo: Foo) {
        console.log(foo.bar?.baz.toUpperCase());
    }
    
    optChain({ bar: { baz: "hello" } }); // HELLO
    optChain({}); // undefined
    

    You should use optional chaining if you are not sure that your property accesses are safe and you want runtime checks to protect you.


    On the other hand, non-null assertions have no effect whatsoever at runtime. It's a way for you to tell the compiler that even though it cannot verify that a property exists, you are asserting that it is safe to do it. This also has the effect of stopping the compiler from complaining, but you have now taken over the job of ensuring type safety. If, at runtime, the value you asserted as defined is actually undefined, then you have lied to the compiler and you might hit a runtime error:

    function nonNullAssert(foo: Foo) {
        console.log(foo.bar!.baz.toUpperCase());
    }
    
    nonNullAssert({ bar: { baz: "hello" } }); // HELLO
    nonNullAssert({}); // 💥 TypeError: foo.bar is undefined
    

    You should only use non-null assertions if you are sure that your property accesses are safe, and you want the convenience of skipping runtime checks.


    Playground link to code