1

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>
)
1

2 Answers 2

3

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

Sign up to request clarification or add additional context in comments.

6 Comments

You could add explicitly the type on exercise.
const exercice: Exercice = { description:'Please select an exercise', title: 'Welcome!', ...props }
I fixed it to support other props
I'd still be curious to know if I can type my exercise prop to be strictly an empty object, or an object with all the types I defined as required. Something more precise an object with optional properties.
My solution so far is to keep the Partial<Exercise> on the type definition of props. Then, in the body of my component, I destructure my other props like so const { exercises, category, onSelect } = props. To access and type the exercise prop, I use your solution, adding a Partial<Exercise> type on the object.
|
1

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

2 Comments

Thanks a lot, I refactored this component and what the parent was passing! This Partial<Exercise> & Required<Pick<Exercise, 'description' | 'title'>> was definitely what I had in mind, but as a TS begginer, I couldn't find the proper syntax. The destructuring with ternary values to assign defaults is a really neat solution. Thumbs up!
Don't you think that having both null and undefined available in JavaScript can lead to confusion? I read about it from TSLint documentation: palantir.github.io/tslint/rules/no-null-keyword I would like to hear more opinions on this subject to identify good practices...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.