1

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?

HynekS
  • 2,738
  • 1
  • 19
  • 34
  • That actually looks really atypical to me. You're destructuring `ExampleProps` from the props provided in the parameter list, then destructuring it again into individual parts. Normally those individual parts would be individual props. I would expect it to be `function Example(ExampleProps)`, not `function Example({ ExampleProps })`. – T.J. Crowder Nov 11 '21 at 13:29
  • @T.J. Crowder Yes, I was thinking that might **not** be the most idiomatic example. These are, let's say, 'data props' (coming from NextJS `getStaticProps`). But destructuring them earlier doesn't change much, or does it? – HynekS Nov 11 '21 at 13:35
  • No, not really I suppose. :-) – T.J. Crowder Nov 11 '21 at 13:38

1 Answers1

1

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

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Thank you. The first option (Prop by Prop) look a little tedious (some of the properties are deeply nested) and possibly hard to maintain, but the utility function looks super cool! I was secretly thinking of converting `null`s to `undefined`s, but I wasn't sure if the idea isn't completely ridiculous. Definitely will try to implement! – HynekS Nov 11 '21 at 13:53
  • @HynekS - My pleasure? Was a fun problem. I've added a playground link just FWIW. – T.J. Crowder Nov 11 '21 at 13:57