#How can I assert that data is not undefined in children components

80 messages · Page 1 of 1 (latest)

magic ridge
#

This is a small example

function Loading({
  data,
  children,
}: PropsWithChildren<{ data: number | undefined }>) {
  if (!data) return <></>;
  return children;
}

function My({ data }: { data: number }) {
  return <div>{data}</div>;
}

function Home() {
  const data: number | undefined = Math.random() > 0.5 ? 1 : undefined;
    
  return (
    <Loading data={data}>
      <My data={data}></My>
    </Loading>
  );
}

In this Loading component is taking data and based on it's value, it's rendering the children

Logically, I know that data will never be undefined inside Loading Node, but typescript still considers it as undefined for My Component.

How can I assert that data is not undefined without doing data! or data && <My data={data}/>

can it be done without passig data as well?

celest beacon
#

Well both your Loading and Data element are defined in Home, so both the type of data from the definition in Home

#

Either you narrow the type within Home, taking that logic out of Loading and putting it in Home

#

Or you pass a callback function to Home so that it can call it with the appropriate data type

#

or you pass the My Component type to Loading and let Loading create the element

fallow knollBOT
#
sandiford#0

Preview:```ts
import React from "react"

type DataComponent = (args: {
data: number
}) => React.ReactNode

type LoadingProps = {
data: number | undefined
ChildComponent: DataComponent
}
function Loading({
data,
ChildComponent,
}: LoadingProps) {
if (!data) return <></
...```

celest beacon
#

I defined DataComponent as (args: { data: number }) => React.ReactNode, but there will be a better way to define it, that accepts both class and function components, but that's fine for the example

#

I think this is correct:

fallow knollBOT
#
sandiford#0

Preview:ts ... type DataComponent = React.ComponentType<{ data: number }> ...

#
sandiford#0

Preview:```ts
import React from "react"

type LoadingProps = {
data: number | undefined
children: (data: number) => React.ReactNode
}
function Loading({data, children}: LoadingProps) {
if (!data) return <></>
return children(data)
}

function My({data}: {data: number})
...```

celest beacon
#

Here is the callback approach

#

Essentially both ways you are postponing the actual creating of the My element until after Loading runs, either because the callback doesn't run until later, or the Loading element is the one to create the component instance after receiving the component as a prop.

celest beacon
#

@magic ridge

magic ridge
#

sorry for being late, will check the playgrounds

#

what I wrote was just a generic example,
What I was trying to figure out something was that, is it possible in typescript to assert a type for variables inside their scope

  const data: number | undefined;
  <Component>
    // Assert here that data is not undefined
  </Component>

There are something like this I have done, and thought I could replicate it for components as well

const isNotNullOrUndefined = <T extends unknown>(
  value: T
): value is Exclude<T, undefined | null> =>
  !(value === null || value === undefined);

...

const data: number | undefined;

if (isNotNullOrUndefined) {
  // data is number here
}
celest beacon
#

You can also use type guards like that, or simple narrowing, in a component

#
    <Loading data={data}>
      {(typeof data === 'number') && <My data={data}></My>}
    </Loading>

function isNumber(u: unknown): u is number {
  return typeof u === 'number'
}

    <Loading data={data}>
      {(isNumber(data)) && <My data={data}></My>}
    </Loading>

// don't do this one
function assertNumber(u: unknown): asserts u is number {
  if (typeof u !== 'number') throw new Error(`Expected "${u}" to be a number`)
}
    <Loading data={data}>
      {(assertNumber(data)) || <My data={data}></My>}
    </Loading>
#

But with each of these you are either assuming responsibility for the type of data (the first one), and so are essentially disabling type checking and safety

#

Or you are double writing your logic, which increases maintainance and could leads to flaws if both channels are not kept in sync

#

Though TS is likely to catch a lot of such issues.

clever echo
#

@magic ridge It's actually not correct to try to assert that data is defined inside <Loading> - that isn't how React works.

#

When you do <Loading data={data}><My data={data} /></Loading> React is going to evaluate <My data={data} /> regardless of what <Loading> does under the hood.

#

The contents of the Loading component is always executed regardless of whether React actually renders those contents to the DOM.

#

JSX is basically a special object syntax, so your code here, if we take the JSX out, looks like:

return {
    type: Loading,
    props: {
       data: data, // number | undefined
       children: {
           // the <My data={data} component>
           type: My,
           props: data // number | undefined, because this executes regardless
       }
    }
}
magic ridge
#

so the proper way to do that would be

<Loading data={data}>
{data && <My data={data} />}
</Loading>
magic ridge
clever echo
celest beacon
# magic ridge so the proper way to do that would be ```tsx <Loading data={data}> {data && <My...

@magic ridge
No, the proper way is one of the ways I showed first, unless you can do what Retsam says, but often you want more flexibility.

You want to create a flow from top to bottom. When you pass <My data={data} /> to <Loading> you are breaking that flow, by defining the contents before Loading has run. This is fine in simple cases, but when there is conditional stuff going on you want to create that top down flow. That's why you either pass My to Loading to let Loading create the My element with the right data, or you pass Loading a callback, so that Loading can run that callback after it's code runs.

celest beacon
# magic ridge so the proper way to do that would be ```tsx <Loading data={data}> {data && <My...

this is a variant of

  <Loading data={data}>
    {(typeof data === 'number') && <My data={data}></My>}
  </Loading>

It's bad practice because you have the same logic in 2 different places. If you want to change the logic then you have to make 2 updates, and forgetting can lead to bugs - if your check inside and outside end up different you could end up passing false as the children to Loading, when the Loading checks passes, then you've got a bug to track down (most such bugs would end up caught by TS somewhere, but still).
It's the DRY principle - Don't Repeat Yourself.

celest beacon
# fallow knoll

This ways is gonna be the most flexible. As Loading just gives the data: number value and then the function can return any React content.

celest beacon
#

While your approach adds a check, and a potential risk of bugs from that check. Performance wise I don't suppose it matters though, perhaps the first one is best, but you shouldn't worry about that.

#

Retsam is correct in saying not to use the assert example, because it would just throw an error when data is undefined. 😓

Bad:

function assertNumber(u: unknown): asserts u is number {
  if (typeof u !== 'number') throw new Error(`Expected "${u}" to be a number`)
}
  <Loading data={data}>
    {(assertNumber(data)) || <My data={data}></My>}
  </Loading>
#

But yeah, you just avoid those issues if you follow a correct data flow.

fallow knollBOT
#
sandiford#0

Preview:```ts
const number: number | undefined = 0

console.log(number && "it's a number")```

celest beacon
#

Se this example. 0 is a number but it's still falsey

#

I suggest not using truthy/falsy checks, they are another way to get bugs. Check typeof or not null / undefined.

magic ridge
celest beacon
#
<Loading data={data}>
    {isNotNullOrUndefined(data) && <My data={data}></My>}
</Loading>

Are you gonna do this then? 🙂

magic ridge
# celest beacon <@766553144938201089> No, the proper way is one of the ways I showed first, unl...

From what I understand it, I will sumarize

  1. If I do
  <Loading data={data}>
    <My data={data} />
  </Loading>

My function will still be executed even though I am not rendering children and will throw error. So that's bad practice

Instead if I do

  <Loading data={data}>
    {(data) => <My data={data} />}
  </Loading>

I am passing a function that renders My component and it will be only called when Loading will render the children
But Is it a good coding practice, I have never seen someone do it, though I don't have much experience under the cap

magic ridge
magic ridge
celest beacon
#
<Loading data={data}>
  <Inner data={data} />
</Loading>

This doesn't run the Inner function. It transforms to

jsx(Loading, { 
  children: [
    jsx(Inner, { data: data })
  ]
})

Which are function calls that return:

{
  type: Loading
  props: {
    children: [
      {
        type: Inner,
        props: {
          data: data
        }
      }
    ]
  }
}
#

So the Components (functions or classes) are passed by reference, and Inner is never actually instantiated or run if Loading does render the Inner as a child

#

So, technically its a type error but fine at run time

#

Of course you don't want that, because if you make a mistake in your logic TS can't catch it for you

#

if you just tell TS it's a number or something like that

#

I am passing a function that renders My component and it will be only called when Loading will render the children
This is exactly right

#

And the nice thing about a callback, is that the callback can be programmed to accept only a number

{(data: number) => <My data={data} />}
#

So you get the type guarantee here, and also when you call the callback from Loading it gets typechecked, so you can't make a mistake

magic ridge
#

What I am looking with all the Loading Thing is I want to make a common component wrapper that render skeleton loading when data is undefined
And then I just wrap my component in it

celest beacon
#

Which isn't really ideal

#

Although, it might already accept false, because false is a kind of ReactNode

#

So I guess it depends how you type children

celest beacon
#

There must be lots of places where callbacks are used. I think it was in react-router

#
function FirstPage() {
  // ...
  return (
    <>
      <Link to='/secondpage'>go to second page</Link>
      <Route
        path='/secondpage'
        render={() => <SecondPage myCallback={selectedPlaylist}}
      />
   </>
  );
}
#

and React Context?

#

Yeah

#
const withExample = (Component) => (props) =>
  (
    <ExampleContext.Consumer>
      {(example) => (
        <Component {...props} example={example} />
      )}
    </ExampleContext.Consumer>
  );

React Context uses it to pass the context to children of the Consumer

magic ridge
#

I guess than it's not a bad practice
I can do that
Do you think a componennt should be able to handle it's own undefined props state instead of external logic handling it?

#

because when I make small reusable components, i designed them to have minimal logic in themselves, and they should just do what they intend do, nothing more

celest beacon
#

I would make a Component have clear requirements about what to pass it

#

If it's not meant to be undefined, don't allow undefined

#

Otherwise what do you do when undefined is received? Just show nothing? If you use a Component expecting it to render, and it shows nothing because data is undefined, then that's probably not what you want, and now it didn't tell you straight away.

#

So I would not accept anything that isn't valid, because you want to get red squiggles ASAP to fix the issue ASAP

#

Obviously it depends on context. Sometimes you want to be able to handle undefined data, maybe that is somehow valid.

magic ridge
#

I understand, I try to do that most times

fallow knollBOT
#

@celest beacon Here's a shortened URL of your playground link! You can remove the full link from your message.

sandiford#0

Preview:```ts
import React from "react"
type ActualContent = Exclude<
React.ReactNode,
boolean | null | undefined

// ^?```

celest beacon
#

React doesn't render booleans, null, undefined or empty string

#

I couldn't remove empty string, because we can't remove a subset of string, so that's the best I got.

#

🤷🏻‍♂️

magic ridge
#

thanks for all the help

#

I have lot to learn

#

!resolved