javascripttypescriptoptional-chainingnullish-coalescing

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


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?


Solution

  • 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