In ReactJS, I commonly use this pattern of destructurnig props (I suppose it is quite idiomatic):
export default function Example({ ExampleProps }) {
const {
content,
title,
date,
featuredImage,
author,
tags,
} = ExampleProps || {};
I can add default values while destructuring, which adds some safety:
export default function Example({ ExampleProps }) {
const {
content = "",
title = "Missing Title",
date = "",
featuredImage = {},
author = {},
tags = [],
} = ExampleProps || {};
But now I switched to TypeScript strict
mode and I have quite a hard time. My props are typed by GraphQl codegen, and virtually all the properties are wrapped in a Maybe<T>
type, so when unwrapped, there are like actualValue | null | undefined
.
The default values ({ maybeUndefined = ""} = props)
can save me in case the value is undefined
, but the null
values would fall through, so the TS compiler is nagging and my code results in a lot of:
tags?.nodes?.length // etc…
which makes me a little nervous because of the The Costs of Optional Chaining article (although I don't know how relevat it still is in 2021). I've also heard ?.
operator overuse being referred as an example of "code smell".
Is there a pattern, probably utilizing the ??
operator, that would make the TS compiler happy AND could weed out at least some of that very?.long?.optional?.chains
?
I see two possible options:
Do the nullish coalescing property-by-property, or
Use a utility function
Fairly plodding (I'm a plodding developer):
// Default `ExampleProps` here −−−−−−−−−−−−−−−vvvvv
export default function Example({ ExampleProps = {} }) {
// Then do the nullish coalescing per-item
const content = ExampleProps.content ?? "";
const title = ExampleProps.title ?? "Missing Title";
const date = ExampleProps.date ?? "";
const featuredImage = ExampleProps.featuredImage ?? {},
const author = ExampleProps.author ?? {},
const tags = ExampleProps.tags ?? [];
// ...
Alternatively, use a utility function along these lines to convert null
values (both compile-time and runtime) to undefined
, so you can use destructuring defaults when destructuring the result. The type part is fairly straightforward:
type NullToUndefined<Type> = {
[key in keyof Type]: Exclude<Type[key], null>;
}
Then the utility function could be something like this:
function nullToUndefined<
SourceType extends object,
ResultType = NullToUndefined<SourceType>
>(object: SourceType) {
return Object.fromEntries(
Object.entries(object).map(([key, value]) => [key, value ?? undefined])
) as ResultType;
}
or like this (probably more efficient in runtime terms):
function nullToUndefined<
SourceType extends object,
ResultType = NullToUndefined<SourceType>
>(object: SourceType) {
const source = object as {[key: string]: any};
const result: {[key: string]: any} = {};
for (const key in object) {
if (Object.hasOwn(object, key)) {
result[key] = source[key] ?? undefined;
}
}
return result as ResultType;
}
Note that Object.hasOwn
is very new, but easily polyfilled. Or you could use Object.prototype.hasOwn.call(object, key)
instead.
(In both cases within nullToUndefined
I'm playing a bit fast and loose with type assertions. For a small utility function like that, I think that's a reasonable compromise provided the inputs and outputs are well-defined.)
Then:
export default function Example({ ExampleProps }) {
const {
content = "",
title = "Missing Title",
date = "",
featuredImage = {},
author = {},
tags = [],
} = nullToUndefined(ExampleProps || {});
// ^^^^^^^^^^^^^^^^−−−−−−−−−−−−−−−−−−^
// ...