Is there a way to utilize the nullish coalescing operator (`??`) in object property destructuring?

1.4k views Asked by At

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?

1

There are 1 answers

2
T.J. Crowder On BEST ANSWER

I see two possible options:

  1. Do the nullish coalescing property-by-property, or

  2. Use a utility function

Property by property

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 ?? [];
    // ...

Utility function

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 || {});
    //  ^^^^^^^^^^^^^^^^−−−−−−−−−−−−−−−−−−^
    // ...

Playground link