Reputation: 4182
With TypeScript 2.9.1 and strictNullChecks is enabled, the compiler tells me Object is possibly undefined.
interface A {
b?: B;
}
interface B {
c: C;
}
interface C {
d: string;
}
const a: A = {
b: {
c: {
d: 'foo'
}
}
}
if (a && a.b) { // I don't want to write this a lot.
console.log(a.b.c.d)
}
console.log(a!.b!.c.d) // this is actually ignoring the warning.
console.log(a.b.c.d) // => Object is possibly undefined.
How can we write better way? I can't write guard function too. Link to playground.
const hasB = (arg: A) => arg && arg.b;
if (hasB(a)) {
console.log(a.b.c.d) => Object is possibly undefined.
}
Upvotes: 2
Views: 500
Reputation: 1717
Obviously the compiler warning is correct, it does exactly what the strictNullChecks
options is supposed to do, protects you.
So if you are sure your a.b
will never be undefined, you should fix your types.
Sometimes you get your types from protobuf definitions, or some other shared project. Let's say you can't modify the types. You can still create new types from them using mapped types and conditional types (need typescript 2.8):
type NotNullableDeep<T> = T extends Array<infer P> ? NotNullableArray<P> : T extends Object ? NotNullableDeepObject<T> : NonNullable<T>;
type NotNullableDeepObject<T> = {[K in keyof T]-?: NotNullableDeep<NonNullable<T[K]>>};
interface NotNullableArray<T> extends Array<NotNullableDeep<T>> { }
This way, if you do:
const a: NotNullableDeep<A> = {
b: {
c: {
d: 'foo'
}
}
}
the compiler won't shout on console.log(a.b.c.d)
;
But this is assuming you are 100% sure at runtime a.b
will never be undefined.
What happens if it can be undefined
and you just want to avoid constant if (a && a.b)
checks?
One great option is the Maybe monad. it's a powerful abstraction, however for simple use cases like just getting a.b.c.d
it can be a bit cumbersome. (Lot's of implementations out there, just choose one of them)
Another option I can (kinda) suggest, is abusing the es6 proxy. We use proxy to handle the case when the key doesn't exist.
const a: A = {
b: {
c: {
d: 'foo'
}
}
}
function isUndefined<T>(o: T | undefined): o is undefined {
return typeof o === 'undefined';
}
const generateSafeObject = <T extends Object>(value: T): NotNullableDeep<T> => {
return new Proxy(value, {
get: <K extends keyof T>(target: T, prop: K) => {
return isUndefined(target[prop]) ? getAllProxy<T[K]>({__undefined: true} as any) : getAllProxy<T[K]>(target[prop]);
}}) as any;
}
const safeA = generateSafeObject(a);
console.log(safeA.b.c.d)
There are noticeable issues with this approach, I haven't checked in a long time but at least in the past es6 proxies had performance issues. Also if let's say a.b
is indeed undefined, if you console.log(a.b)
you won't see undefined, you'll see an empty object {}
. But for simple use cases such as data fetched from a server I think it can be good.
Final suggestion is fixing your guard function, it should be something like this:
const hasB = (arg: A | {b: B}): arg is {b: B} => !!(arg && arg.b);
if (hasB(a)) {
console.log(a.b.c.d)
}
Upvotes: 1
Reputation: 3153
If you define an optional property in your interface, you have to deal with the check, IF this property is defined in the specific case.
Regarding to your example, you could return if a.b
is undefined (no need to check for a
here, because you declared it right before):
if(!a.b) {
return;
}
console.log(a.b.c.d);
Upvotes: 0