Safe navigation operator (?.) or (!.) and null property paths

In Angular 2 templates safe operator ?. works, but not in component.ts using TypeScript 2.0. Also, safe navigation operator (!.) doesn't work.

For example:

This TypeScript

if (a!.b!.c) { } 

compiles to this JavaScript

if (a.b.c) { } 

But when I run it, I get the follow error:

Cannot read property 'b' of undefined

Is there any alternative to the following?

if (a && a.b && a.b.c) { } 
4

7 Answers

! is non-null assertion operator (post-fix expression) - it just saying to type checker that you're sure that a is not null or undefined.

the operation a! produces a value of the type of a with null and undefined excluded


Optional chaining finally made it to typescript (3.7) 🎉

The optional chaining operator ?. permits reading the value of a property located deep within a chain of connected objects without having to expressly validate that each reference in the chain is valid. The ?. operator functions similarly to the . chaining operator, except that instead of causing an error if a reference is nullish (null or undefined), the expression short-circuits with a return value of undefined. When used with function calls, it returns undefined if the given function does not exist.

Syntax:

obj?.prop // Accessing object's property obj?.[expr] // Optional chaining with expressions arr?.[index] // Array item access with optional chaining func?.(args) // Optional chaining with function calls 

Pay attention:

Optional chaining is not valid on the left-hand side of an assignment

const object = {}; object?.property = 1; // Uncaught SyntaxError: Invalid left-hand side in assignment 
5

Since TypeScript 3.7 was released you can use optional chaining now.

Property example:

let x = foo?.bar.baz(); 

This is equvalent to:

let x = (foo === null || foo === undefined) ? undefined : foo.bar.baz(); 

Moreover you can call:

Optional Call

function(otherFn: (par: string) => void) { otherFn?.("some value"); } 

otherFn will be called only if otherFn won't be equal to null or undefined

Usage optional chaining in IF statement

This:

if (someObj && someObj.someProperty) { // ... } 

can be replaced now with this

if (someObj?.someProperty) { // ... } 

Ref:

2

Update:

Planned in the scope of 3.7 release


You can try to write a custom function like that.

The main advantage of the approach is a type-checking and partial intellisense.

export function nullSafe<T, K0 extends keyof T, K1 extends keyof T[K0], K2 extends keyof T[K0][K1], K3 extends keyof T[K0][K1][K2], K4 extends keyof T[K0][K1][K2][K3], K5 extends keyof T[K0][K1][K2][K3][K4]> (obj: T, k0: K0, k1?: K1, k2?: K2, k3?: K3, k4?: K4, k5?: K5) { let result: any = obj; const keysCount = arguments.length - 1; for (var i = 1; i <= keysCount; i++) { if (result === null || result === undefined) return result; result = result[arguments[i]]; } return result; } 

And usage (supports up to 5 parameters and can be extended):

nullSafe(a, 'b', 'c'); 

Example on playground.

2

Another alternative that uses an external library is _.has() from Lodash.

E.g.

_.has(a, 'b.c') 

is equal to

(a && a.b && a.b.c) 

EDIT: As noted in the comments, you lose out on Typescript's type inference when using this method. E.g. Assuming that one's objects are properly typed, one would get a compilation error with (a && a.b && a.b.z) if z is not defined as a field of object b. But using _.has(a, 'b.z'), one would not get that error.

5

A new library called ts-optchain provides this functionality, and unlike lodash' solution, it also keeps your types safe, here is a sample of how it is used (taken from the readme):

import { oc } from 'ts-optchain'; interface I { a?: string; b?: { d?: string; }; c?: Array<{ u?: { v?: number; }; }>; e?: { f?: string; g?: () => string; }; } const x: I = { a: 'hello', b: { d: 'world', }, c: [{ u: { v: -100 } }, { u: { v: 200 } }, {}, { u: { v: -300 } }], }; // Here are a few examples of deep object traversal using (a) optional chaining vs // (b) logic expressions. Each of the following pairs are equivalent in // result. Note how the benefits of optional chaining accrue with // the depth and complexity of the traversal. oc(x).a(); // 'hello' x.a; oc(x).b.d(); // 'world' x.b && x.b.d; oc(x).c[0].u.v(); // -100 x.c && x.c[0] && x.c[0].u && x.c[0].u.v; oc(x).c[100].u.v(); // undefined x.c && x.c[100] && x.c[100].u && x.c[100].u.v; oc(x).c[100].u.v(1234); // 1234 (x.c && x.c[100] && x.c[100].u && x.c[100].u.v) || 1234; oc(x).e.f(); // undefined x.e && x.e.f; oc(x).e.f('optional default value'); // 'optional default value' (x.e && x.e.f) || 'optional default value'; // NOTE: working with function value types can be risky. Additional run-time // checks to verify that object types are functions before invocation are advised! oc(x).e.g(() => 'Yo Yo')(); // 'Yo Yo' ((x.e && x.e.g) || (() => 'Yo Yo'))(); 

Building on @Pvl's answer, you can include type safety on your returned value as well if you use overrides:

function dig< T, K1 extends keyof T >(obj: T, key1: K1): T[K1]; function dig< T, K1 extends keyof T, K2 extends keyof T[K1] >(obj: T, key1: K1, key2: K2): T[K1][K2]; function dig< T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2] >(obj: T, key1: K1, key2: K2, key3: K3): T[K1][K2][K3]; function dig< T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2], K4 extends keyof T[K1][K2][K3] >(obj: T, key1: K1, key2: K2, key3: K3, key4: K4): T[K1][K2][K3][K4]; function dig< T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2], K4 extends keyof T[K1][K2][K3], K5 extends keyof T[K1][K2][K3][K4] >(obj: T, key1: K1, key2: K2, key3: K3, key4: K4, key5: K5): T[K1][K2][K3][K4][K5]; function dig< T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2], K4 extends keyof T[K1][K2][K3], K5 extends keyof T[K1][K2][K3][K4] >(obj: T, key1: K1, key2?: K2, key3?: K3, key4?: K4, key5?: K5): T[K1] | T[K1][K2] | T[K1][K2][K3] | T[K1][K2][K3][K4] | T[K1][K2][K3][K4][K5] { let value: any = obj && obj[key1]; if (key2) { value = value && value[key2]; } if (key3) { value = value && value[key3]; } if (key4) { value = value && value[key4]; } if (key5) { value = value && value[key5]; } return value; } 

Example on playground.

Versions above typescript 3.7 supports safe navigation operator for typescript < 3.7 I made this function which can be useful.

export function isAccessible(data, keys, start=0) { if (start == 0 && (data == null || data == undefined)) { console.warn("data",data); return false; } else { if (data[keys[start]] == null || data[keys[start]] == undefined) { console.warn("Object valid till", keys.slice(0,start),keys[start],"undefined"); return false; } else { if (start + 1 >= keys.length) { return data[keys[start]]; } return this.isAccessible(data[keys[start]], keys, start + 1); } } } 

function call in code

Suppose we have a Object obj keys of which can vary and we want to check if obj.key1.key2 is accessible or not then function call will be as follows:

isAccessible(Object,["key1","key2"]) 

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

You Might Also Like