26

I want to create a React TypeScript component whose props is a union of two different interfaces. However, when I do so, I get the warning:

TS2339: Property 'color' does not exist on type 'PropsWithChildren<Props>'

How can I create a React TypeScript component with a union of two different prop interfaces and at the same time am able to destructure those props? Thanks!

sampleComponent.tsx:

import * as React from 'react';

interface SamplePropsOne {
  name: string;
}

interface SamplePropsTwo {
  color: string;
}

type Props = SamplePropsOne | SamplePropsTwo;

const SampleComponent: React.FC<Props> = ({ color, name }) => (
  color ? (
    <h1>{color}</h1>
  ) : (
    <h1>{name}</h1>
  )
);

export default SampleComponent;

enter image description here

3
  • 1
    why you don't want to pass only one type of props from parent component to SampleComponent? You can pass <SampleComponent name={name} /> or <SampleComponent name={color} />. Commented Oct 30, 2019 at 18:59
  • 3
    @AndriiGolubenko, please don't take the component implementation details literally. The question is more about how to create a component that can accept a series of different prop interfaces. Commented Oct 30, 2019 at 19:46
  • 1
    I understand that you gave just an example. But what you want to create is not a component. It is like a fabric of components, that accepts type, data and then renders the desired component. Commented Oct 30, 2019 at 21:12

3 Answers 3

29

Before TypeScript will let you read name or color off the union type, it needs some evidence that you're working with the correct type of Props (SamplePropsOne or SamplePropsTwo). There are a few standard ways to provide it with this.

One is by making the union a tagged union by introducing a property to distinguish branches of the union. This type checks just fine:

interface SamplePropsOne {
  type: 'one';
  name: string;
}

interface SamplePropsTwo {
  type: 'two';
  color: string;
}

type Props = SamplePropsOne | SamplePropsTwo;

const SampleComponent: React.FC<Props> = props => (
  props.type === 'one' ? (
    <h1>{props.name}</h1>
  ) : (
    <h1>{props.color}</h1>
  )
);

If you get the cases backwards (as I did when writing this!) then TypeScript will complain.

If the presence of a property is enough to distinguish the types, then you can use the in operator:

interface SamplePropsOne {
  name: string;
}
interface SamplePropsTwo {
  color: string;
}
type Props = SamplePropsOne | SamplePropsTwo;

const SampleComponent: React.FC<Props> = props => (
  'color' in props ? (
    <h1>{props.color}</h1>
  ) : (
    <h1>{props.name}</h1>
  )
);

If determining which type of object you have requires more complex logic, you can write a user-defined type guard. The key part is the "is" in the return type:

function isSampleOne(props: Props): props is SamplePropsOne {
  return 'name' in props;
}

const SampleComponent: React.FC<Props> = props => (
  isSampleOne(props) ? (
    <h1>{props.name}</h1>
  ) : (
    <h1>{props.color}</h1>
  )
);

It's also worth noting that because of the way structural typing works, there's no reason props in your example couldn't have both name and color:

const el = <SampleComponent name="roses" color="red" />;  // ok

If it's important to not allow this, you'll need to use some slightly fancier types:

interface SamplePropsOne {
  name: string;
  color?: never;
}
interface SamplePropsTwo {
  color: string;
  name?: never;
}
type Props = SamplePropsOne | SamplePropsTwo;

The ts-essentials library has an XOR generic that can be used to help construct exclusive unions like this.

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

Comments

21

I think what you are looking for are intersection types.

Replace this line:

type Props = SamplePropsOne | SamplePropsTwo;

with this line:

type Props = SamplePropsOne & SamplePropsTwo;
  • Intersection types: combine multiple interfaces/types into one

  • Union types: choose one of multiple interfaces/types

EDIT

What you want is not possible (i think). What you could do is destructuring every type in a single line after casting props:

const SampleComponent: React.FC<Props> = props => {
  const { name } = props as SamplePropsOne;
  const { color } = props as SamplePropsTwo;

  return color ? <h1>{color}</h1> : <h1>{name}</h1>;
};

6 Comments

I want a user to be able to provide a name prop or a color prop, but not both.
@Jimmy oh, sorry. I edited the question. It's a bit more code but I dont think there is another option here. Also you might want to consider implementing 2 different components which use a 3 common component.
@ysfaran, Any idea to do this? -> "I want a user to be able to provide a name prop or a color prop, but not both." ? ,Thanks!
@Esther-I I think it should be clear from the answer. To break it down, you need to use union types: type Props = { name: string } | { color: string }. I don't know about your exact issue, so I can not answer it properly. Consider asking a new question!
Sorry, you're right. It was supposed to be directed to @Jimmy, he had the same issue. Thanks!
|
1

I think this will help

interface SamplePropsOne {
  name: string;
  color: never;
}

interface SamplePropsTwo {
  name: never;
  color: string;
}

type Props = SamplePropsOne | SamplePropsTwo;

const SampleComponent = ({ color, name }: Props) => {
  console.log(color ? color : name);
}

ts playground link : https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgMpwLYAcA2EAKUA9lgM4DyIKA3gFDLIiYQBcypYUoA5gNz3IERHEShsqAN2j8AvrVqhIsRCnTY8hEqQAqAdyLI6DJhlaMIUqPwZCRY9px6z5YAJ5YUmssgC8aTLgExGSUKAA+-upBWnpE-LRCIByRgQDCRNhEVOC+yAAU1ILCogA0jMzIMmxepACUvgB8hgKJpMIQAHQi3Hm2osgA-EV2yOLMtbJAA

Comments

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.