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
Answer from jcalz on Stack OverflowShould we use ! (non-null assertion) or ?. (optional chaining) in TypeScript?
How to enforce the use of optional chaining for any type object in TypeScript?
Using optional chaining operator for object property access
[AskJS] Is it a problem if the code base is filled with optional chaining?
Videos
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
Those are completely different things.
The null assertion operator !. is you, the programmer, asserting to the compiler that you know for a fact that the property access cannot fail for reasons the compiler cannot prove. It is no more safe from a runtime error than any other assertion that you the programmer make to the compiler that you know better that it does.
const foo = null;
foo!.someProperty; // compiles but you will get a TypeError! Cannot read property 'someProperty' of null or undefined.
The other is the optional chaining operator. It is basically shorthand for this common Javascript pattern:
const something = foo && foo.bar && foo.bar.baz;
But it's better than just shorthand, because the above will fail if one of those values is falsey but some falsey values support property access. With the optional property accessor you just write:
const something = foo?.bar?.baz;
And you're done. And just like the Javascript version it's "safe" in that it's guaranteed not to result in a runtime error from trying to access a property of a null reference.
In the particular case you have there, you probably want something like this:
const enteredText = textInputRef.current?.value || '';
Where it's very clear that the result is a string no matter what.
I'm trying to enforce the use of optional chaining for accessing properties of any type object in TypeScript to avoid potential runtime errors. For example, I want the following code to throw a TypeScript error:
catch(e: any) {
const code = e.code; // This should throw a TypeScript error
}But this code should not throw an error:
catch(e: any) {
const code = e?.code; // This should not throw a TypeScript error
}Is there a way to configure TypeScript to enforce this rule or any workaround to achieve this behavior?
When accessing a property using bracket notation and optional chaining, you need to use a dot in addition to the brackets:
const value = a?.[b]?.c;
This is the syntax that was adopted by the TC39 proposal, because otherwise it's hard for the parser to figure out if this ? is part of a ternary expression or part of optional chaining.
The way I think about it: the symbol for optional chaining isn't ?, it's ?.. If you're doing optional chaining, you'll always be using both characters.
The Optional Chaining operator is ?.
Here are some examples for nullable property and function handling.
const example = {a: ["first", {b:3}, false]}
// Properties
example?.a // ["first", {b:3}, false]
example?.b // undefined
// Dynamic properties ?.[]
example?.a?.[0] // "first"
example?.a?.[1]?.a // undefined
example?.a?.[1]?.b // 3
// Functions ?.()
null?.() // undefined
validFunction?.() // result
(() => {return 1})?.() // 1
Bonus: Default values
?? (Nullish Coalescing) can be used to set a default value if undefined or null.
const notNull = possiblyNull ?? defaultValue
const alsoNotNull = a?.b?.c ?? possiblyNullFallback ?? defaultValue
Jumping into a new code base and it seems like optional chaining is used EVERYWHERE.
data?.recipes?.items
label?.title?.toUpperCase();
etc.
It almost seems like any time there is chaining on an object, it always includes the ?.
Would you consider this an anti-pattern? Do you see any issues with this or concerns?
Firstly the TS compiler complains about Object is possibly 'undefined'.ts(2532) at obj. I don't get it since it looks to me like the object is indeed defined, it is just according to the interface the b and c property can be undefined.
Yes, compiler shows that b can be undefined. This message is not about the a. You can easily check this by changing if (obj.a === 1) - no error now.
The construction if (obj.a.b?.c === 1) will be converted to if (((_a = obj.a.b) === null || _a === void 0 ? void 0 : _a.c) === 1) after compile. I prefer using old good guards:
Copyif (obj.a.b && obj.a.b.c === 1) {
console.log("here");
}
However this time the compiler says there is a syntax error
This is strange. Since TS 3.7 this should not be an error. Take a look on another online sandbox: click. You will see there is no error here.
To extend the answer by @Anton
You are using parcel bundler in your sample. You need to tell parcel which version of typescript to use (the one that supports optional chaining operator) and the error will go away.
package.json:
Copy"devDependencies": {
"parcel-bundler": "^1.6.1",
"typescript": "^4.0.2"
},
At time of writing, TypeScript does not support the optional chaining operator. See discussion on the TypeScript issue tracker: https://github.com/Microsoft/TypeScript/issues/16
As a warning, the semantics of this operator are still very much in flux, which is why TypeScript hasn't added it yet. Code written today against the Babel plugin may change behavior in the future without warning, leading to difficult bugs. I generally recommend people to not start using syntax whose behavior hasn't been well-defined yet.
Update Oct 15, 2019
Support now exists in [email protected]
Say thanks to https://stackoverflow.com/a/58221278/6502003 for the update!
Although TypeScript and the community are in favor of this operator, until TC39 solidifies the current proposal (which at the time of this writing is at stage 1) we will have to use alternatives.
There is one alternative which gets close to optional chaining without sacrificing dev tooling: https://github.com/rimeto/ts-optchain
This article chronicles what the creators were able to achieve in trying to mirror the native chaining operator:
- Use a syntax that closely mirrors chained property access
- Offer a concise expression of a default value when traversal fails
- Enable IDE code-completion tools and compile-time path validation
In practice it looks like this:
import { oc } from 'ts-optchain';
// Each of the following pairs are equivalent in result.
oc(x).a();
x && x.a;
oc(x).b.d('Default');
x && x.b && x.b.d || 'Default';
oc(x).c[100].u.v(1234);
x && x.c && x.c[100] && x.c[100].u && x.c[100].u.v || 1234;
Keep in mind that alternatives like this one will likely be unnecessary once the proposal is adopted by TypeScript.
Also, a big thanks to Ryan Cavanaugh for all the work you are doing in advocating this operator to TC39!