raxell
raxell

Reputation: 677

React: make typescript infer children type based on conditions in another component

I want to write a component that render its children only if some given prop is not null (e.g. to render a loader while fetching some data and update with the actual data when fetch completes). This is pretty simple:

import React, { ReactElement, ReactNode, useState } from 'react'

type TState = {
    data: {
        text: string
    } | null
}

type NotNullProps = {
    children: ReactNode
    state: TState
}

const NotNull = ({ children, state }: NotNullProps) => {
    if (!state.data) {
      return null
    }

    return <>{children}</>
}

Now, what if I want to use this component this way?

const Example = () => {
    const [state] = useState<TState>({ data: null })

    return (
        <NotNull state={state}>
            <div>{state.data.text}</div> // <--- "Object is possibly 'null'."
        </NotNull>
    )
}

Typescript correctly signals that state.data can be null because it can't infer what is happening in NotNull, but state.data will never be null since NotNull checks for it.
I know this can be solved with render props but they add a bit of clutter to the code. So, is there a way to type NotNull in such a way that typescript does not complains/can infer what is happening?

Here's a playground to try it: https://tsplay.dev/zwORWq

(Yes, I know about Suspense and other stuff but it is not relevant to this question. I just want to know if there's a way to type this component in a way that typescript can infer that the value is not null (again, without render props)).

Upvotes: 1

Views: 302

Answers (1)

awarrier99
awarrier99

Reputation: 3855

You will still have to check for whether state.data is null because even though your NotNull component prevents the children from being rendered, your code will still result in a TypeError with your usage if state.data is null, because the line state.data.text in your code will produce TypeError: Cannot read property 'text' of null when it is interpreted, whether or not the children render. I would suggest just changing the lines to:

return (
    <NotNull state={state}>
        <div>{state.data && state.data.text}</div>
    </NotNull>
)

But of course, this would negate the need for the NotNull component in the first place, except that it also prevents the <div> in your example from being rendered

Upvotes: 1

Related Questions