简体   繁体   中英

Typescript type error while destructuring on empty object with default values?

I'm writing a web application in ReactJS + Typescript. I've a functional component defined like below.

My problem is the following: in the props, for the property exercise , the parent component is passing an object, either initialized empty or of a certain type that I specify, Exercise . Then Typescript raises the following errors:

[ts] Property 'description' does not exist on type '{} | Exercise'
[ts] Property 'title' does not exist on type '{} | Exercise'

How could I refactor it so that if the object is indeed empty, it will use the default values, and otherwise, use the values passed?

EDIT: Added the other props that I use

type Exercise = {
  description: string
  id: string
  muscles: string
  title: string
}

type Props = {
  category: string
  children?: never
  exercise: {} | Exercise
  exercises: Array<[string, Exercise[]]>
  onSelect: (id: string) => void
}

const Exercises = ({
  exercises,
  category,
  onSelect,
  exercise: {
    description = 'Please select an exercise',
    title = 'Welcome!'
  }
}: Props) => (
   <Grid container>
     <Grid item sm>
       {/* Some stuff going on with exercises, category and onSelect */ }
     </Grid>
     <Grid item sm>
       <Paper>
         <Typography variant="h4">{title}</Typography>
         <Typography variant="subtitle1">{description}</Typography>
       </Paper>
     </Grid>
   </Grid>
)

I think something similar to this should work

type Exercise = {
  description: string
  id: string
  muscles: string
  title: string
}

type Props = {
  exercise: Partial<Exercise>
}

const Exercises = (props: Props) => {
    const exercice = {
      description:'Please select an exercise',
      title: 'Welcome!', 
      ...props.exercise
    }

    return (
        <Grid container>
          <Grid item sm>
            <Paper>
              <Typography variant="h4">{exercice.title}</Typography>
              <Typography variant="subtitle1">{exercice.description}</Typography>
            </Paper>
          </Grid>
        </Grid>
    )
}

edit: align code

So overall I don't think your API design is correct for this component. You're basically misusing exercise entity as some default "Welcome message stuff", which is rather miss leading to consumers of this component.

What I would do, is to provide these intro defaults when there is no exercise present, but would definitely not use exercise prop to assign those defaults.

Next thing, don't use {} , that's not empty object (you can define empty object like following https://github.com/Hotell/rex-tils/blob/master/src/guards/types.ts#L39 ) . It used to be a bottom type prior to TS 3.0 ( now unknown is bottom type ). What does it mean? {} can be anything except null/undefined:

// all of this is valid !
let foo: {} = 1231
foo = true
foo = { baz: 'bar' }
foo = [1,2,3]

Also if you really wanna support passing "empty" non primitive data types to components, prefer null :

type Props = {
  category: string
  children?: never
  // Maybe type
  exercise: null | Exercise
  exercises: [string, Exercise[]][]
  onSelect: (id: string) => void
}

Anyways if your really wanna keep your API as is. You have following option:

  1. Extract defaults to constant which needs to be cast to Exercise
const defaultExercise = {
  description: 'Please select an exercise',
  title: 'Welcome!',
} as Exercise
  1. you need to type narrow exercise prop outside function default parameter, as that's not possible within function params
const Exercises = ({ exercises, category, onSelect, exercise }: Props) => {
  // $ExpectType Exercise
  const { title, description } = exercise ? exercise : defaultExercise

  return <>your markup</>
}

Now while this works it gives you false assumptions. As your exercise might be a partial one (if defaults are used), which may lead to runtime errors. You'll need additional type narrowing via guards ( if, ternary ).

You can improve this situation on type level, by some type mappings:

// $ExpectType  { description: string, title: string, id?: string, muscles?: string }
const defaultExercise = {
  description: 'Please select an exercise',
  title: 'Welcome!',
} as Partial<Exercise> & Required<Pick<Exercise, 'description' | 'title'>>

With that type if you would use id or muscles within your component, you'll get proper types as they might be undefined, which mirrors correctly our ternary

const { 
  title, //$ExpectType string 
  description, //$ExpectType string
  id, //$ExpectType string | undefined  
} = exercise ? exercise : defaultExercise

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM