React TypeScript component with two different prop interfaces - reactjs

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;

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.

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>;
};

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

Related

Interface for a React Button component that can be an anchor tag or a button tag

I'm trying to create a button component that can be an anchor tag with a href or a button tag with a type prop. I have the following code:
interface IBaseProps {
children: string;
fullSize?: boolean;
theme?: 'primary' | 'secondary' | 'dark';
}
interface ILinkButtonProps extends IBaseProps {
url: string;
type: never;
props?: AnchorHTMLAttributes<HTMLAnchorElement>;
}
interface IButtonProps extends IBaseProps {
type: 'button' | 'submit' | 'reset';
url: never;
props?: ButtonHTMLAttributes<HTMLButtonElement>;
}
export const Button = ({
children,
props,
theme = 'primary',
fullSize = false,
type = 'button',
url,
}: IButtonProps | ILinkButtonProps): JSX.Element => {
const Tag: keyof JSX.IntrinsicElements = url ? 'button' : 'a';
return (
<Tag
className={`${styles.button} ${styles[theme]} ${
fullSize ? styles.fullSize : ''
}`} // not important
{...props}
{...(Tag === "button" ? {type: `${type}`} : {href: url})}
>
{children}
</Tag>
);
};
However, that gives me some typing errors, for instance:
Types of property 'onCopy' are incompatible.
Type 'ClipboardEventHandler | undefined' is not assignable to type 'ClipboardEventHandler | undefined'.
Type 'ClipboardEventHandler' is not assignable to type 'ClipboardEventHandler'.
Type 'HTMLAnchorElement' is missing the following properties from type 'HTMLButtonElement': disabled, form, formAction, formEnctype, and 11 more.
Is there a way for me to format my code with typescript, so I can achieve a component that allows me to have both a button or a link?
Having the separate Tag variable is your downfall. Typescript is not intelligent enough to "see through" this variable and use it as a union discriminator. Once you assign to to a diff variable, the context of its meaning is lost.
You have to be quite explicit and pass the whole props into your new type guard, then consume that immediately after without assigning flags that you use later.
You will need strict null checks on for this to work. Heres a code sandbox https://codesandbox.io/s/heuristic-haslett-ovqkcw
import React from "react";
type IBaseProps = {
children: string;
fullSize?: boolean;
theme?: "primary" | "secondary" | "dark";
};
type ILinkButtonProps = IBaseProps & {
url: string;
type?: never;
props?: React.AnchorHTMLAttributes<HTMLAnchorElement>;
};
type IButtonProps = IBaseProps & {
type: "button" | "submit" | "reset";
url?: never;
props?: React.ButtonHTMLAttributes<HTMLButtonElement>;
};
export const Button = (props: ILinkButtonProps | IButtonProps): JSX.Element => {
const { children, theme, fullSize, type, url } = props;
const commonProps = {
className: `${styles.button} ${styles[theme]} ${
fullSize ? styles.fullSize : ""
}`
};
if (props.type) {
return (
<button {...commonProps} type={type} {...props.props}>
{children}
</button>
);
}
return (
<a {...commonProps} href={url} {...props.props}>
{children}
</a>
);
};
BTW, in my opinion, you shouldn't do what you are trying to do. Whilst its possible, this component likely breaks the principle of element of least surprise for the developer. Links and buttons are not semantically the same -- the dev should really make a very active choice.

Fixing types for mapping over object to create React components

I really like uing this pattern for rendering similar components and using a _type prop to distinguish it and pass it down to the correct component.
However, I've found it difficult to add in the types correctly and was wondering if you guys could help. I have some questions;
Is the BlockMap type correct?
What type should I be using for ResolvedBlock?
Or generally, is there a better way of writing the types (without changing this structure?)
import React from 'react'
import { ImageBlock } from '/ImageBlock' // Assume all components are imported from whereever
type BlockType =
| 'imageBlock'
| 'formBlock'
| 'statisticBlock'
| 'videoBlock'
| 'quoteBlock'
interface Block {
_type: BlockType
_key: string
heading?: string
backgroundColor?: string
theme?: 'dark' | 'light'
}
type BlockMap = Record<BlockType, JSX.Element> // Is this type correct?
const blockMap:BlockMap = {
imageBlock: ImageBlock,
formBlock: FormBlock,
statisticBlock: StatisticBlock,
videoBlock: VideoBlock,
quoteBlock: QuoteBlock,
}
interface Props {
className?: string
blocks: Block[]
}
export function BlocksBuilder({
blocks = [],
className = ``,
}: Props):JSX.Element {
return (
<>
{blocks.map(block => {
const ResolvedBlock = blockMap[block._type] // What type should ResolvedBlock be?
if (!ResolvedBlock) return null
return (
<ResolvedBlock
className={className}
block={block}
key={block._key}
/>
)
})}
</>
)
}
It's a good pattern, and your type is close, but you will want to define the shape of your components instead.
type BlockMap = Record<BlockType, (props: any) => JSX.Element>
You could also define the props for Block components, and use that type for each of your components
interface BlockProps {
key: string;
block: Block;
classname: string;
}
type BlockComponent = (props: BlockProps) => JSX.Element;
export const ImageBlock: BlockComponent = (props) => {
return <></>;
};
type BlockMap = Record<BlockType, BlockComponent>

How to best type a TypeScript collection of React wrappers

in my team's application, we have run into a few cases where it would be nice to be able to dynamically compose component wrappers (HOCs) without having to know all the wrapper interfaces ahead of time (mostly for swapping out context providers when large portions of our component tree are rendered from different host containers).
I'm able to write a simple implementation for this, but getting optimal type safety has been... challenging πŸ˜‰. In short, I'd like to be able to declare a collection of wrappers such that when any wrapper components are passed into it, the compiler will enforce that each wrapper component gets its correct props type as well.
Here's some code to illustrate the problem. Essentially, my question is how to define WrapperSet.
import * as React from "react";
export const Container: React.FC = () => {
// these could be any arbitrary wrapper components
const wrappers: WrapperSet = [
[WrapperA, { label: "WrapperA" }],
[WrapperB, { title: "foo" }],
];
const content = <span>Original content</span>;
return wrap(content, wrappers);
};
const WrapperA: React.FC<{ label: string }> = ({ label, children }) => (
<>
<div>WrapperA: {label}</div>
{children}
</>
);
const WrapperB: React.FC<{ title: string }> = ({ title, children }) => (
<>
<div>WrapperB: {title}</div>
{children}
</>
);
function wrap(children: JSX.Element, wrappers: WrapperSet): JSX.Element {
let content = children;
wrappers.forEach((wrapper) => {
const [ComponentType, props] = wrapper;
content = <ComponentType {...props}>{content}</ComponentType>;
});
return content;
}
type WrapperSet = [React.ComponentType, React.PropsWithChildren<{}>][];
πŸ‘† If I hover over WrapperSet here, TS tells me type WrapperSet = [any, any][], which is not at all protective.
Some other definitions of WrapperSet I have tried:
type WrapperSet2 = [React.ComponentType<any>, React.PropsWithChildren<any>][] &
{
[K in number]: WrapperSet2[K] extends [React.ComponentType<infer P>, any]
? [React.ComponentType<P>, React.PropsWithChildren<P>]
: never;
};
πŸ‘† TS interprets this as type WrapperSet2 = [any, any][] & { [x: number]: [any, any]; }, which is no better.
type Wrapper<P> = [React.ComponentType<P>, React.PropsWithChildren<P>];
type PropsFromComponentType<T extends React.ComponentType<any>> =
T extends React.ComponentType<infer P> ? P : never;
type WrapperSet3 = [any, any][] & {
[K in number]: WrapperSet4[K] extends [React.ComponentType<infer C>, any]
? Wrapper<PropsFromComponentType<C>>
: never;
};
πŸ‘† TS says type WrapperSet3 = [any, any][] & { [x: number]: Wrapper<unknown>; }.
It seems like there must be some way to tell TS that the outer WrapperSet array supports any kinds of Wrappers, but each Wrapper must be internally consistent, based on a single props type. Or maybe TS doesn't support this kind of expression. FYI, I am using TS 4.3.
Thanks in advance!
You can do it with a combination of types and a utility function that will enforce the props for the given component:
type ComponentAndProps<C extends React.ElementType<any>> = [C, React.ComponentPropsWithRef<C>];
type WrapperSet = ComponentAndProps< React.ElementType<any> >[];
function makeGroup<C extends React.ElementType<any>>(Component: C, props: React.ComponentPropsWithRef<C>): ComponentAndProps<C> {
return [Component, props];
}
Here's a usage example:
export const Container: React.FC = () => {
// these could be any arbitrary wrapper components
const wrappers: WrapperSet = [
makeGroup(WrapperA, { label: "WrapperA" }),
makeGroup(WrapperB, { title: "foo" }),
makeGroup(WrapperB, {}), // Error: missing 'title' property
];
const content = <span>Original content</span>;
return wrap(content, wrappers);
};
The trick is the makeGroup() helper function that allows TypeScript to infer and enforce the props type for the component.
If you just use the tuple notation then the props end up as any and TypeScript can't enforce the props:
const wrappers2: WrapperSet = [
[WrapperA, { label: "WrapperA" }],
[WrapperB, {}] // BAD - no error, TypeScript can't infer the props type for the component here
];
Final note - React has a number of utility types for extracting the props type from a component.
I chose ComponentPropsWithRef just in case you have a component that uses refs, but adjust as necessary:
React.ComponentProps<Component>
React.ComponentPropsWithRef<Component>
React.ComponentPropsWithoutRef<Component>
You can create type util which will accept two components and produce a tuple of wrappers and corresponding props:
import React, { FC, ComponentProps } from "react";
type Wrap<Comps extends FC<any>[]> = {
[Comp in keyof Comps]: Comps[Comp] extends React.JSXElementConstructor<any>
? [Comps[Comp], ComponentProps<Comps[Comp]>]
: never
}
type WrapperSet = Wrap<[typeof WrapperA, typeof WrapperB]>
export const Container: React.FC = () => {
// these could be any arbitrary wrapper components
const wrappers: WrapperSet = [
[WrapperA, { label: "WrapperA" }],
[WrapperB, { title: "foo" }],
];
const content = <span>Original content</span>;
return wrap(content, wrappers);
}; // compiles
But there is still a problem with:
function wrap(children: JSX.Element, wrappers: WrapperSet): JSX.Element {
let content = children;
wrappers.forEach((wrapper) => {
const [ComponentType, props] = wrapper;
content = <ComponentType {...props}>{content}</ComponentType>;
});
return content;
}
Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.
This is why ComponentType expects an intersection of all props and not a union. TS is unable to figure out which prop correspond to each component in dynamic loop.
In order to fix it, we need to create extra function:
const iteration = <Comp extends React.JSXElementConstructor<any>>(
Comp: Comp,
props: ComponentProps<Comp>,
content: JSX.Element
) => <Comp {...props} >{content}</Comp>
And the whole code:
import React, { FC, ComponentProps } from "react";
type Wrap<Comps extends FC<any>[]> = {
[Comp in keyof Comps]: Comps[Comp] extends React.JSXElementConstructor<any>
? [Comps[Comp], ComponentProps<Comps[Comp]>]
: never
}
type WrapperSet = Wrap<[typeof WrapperA, typeof WrapperB]>
export const Container: React.FC = () => {
// these could be any arbitrary wrapper components
const wrappers: WrapperSet = [
[WrapperA, { label: "WrapperA" }],
[WrapperB, { title: "foo" }],
];
const content = <span>Original content</span>;
return wrap(content, wrappers);
};
const WrapperA: React.FC<{ label: string }> = ({ label, children }) => (
<>
<div>WrapperA: {label}</div>
{children}
</>
);
const WrapperB: React.FC<{ title: string }> = ({ title, children }) => (
<>
<div>WrapperB: {title}</div>
{children}
</>
);
const iteration = <Comp extends React.JSXElementConstructor<any>>(
Comp: Comp,
props: ComponentProps<Comp>,
content: JSX.Element
) => <Comp {...props} >{content}</Comp>
function wrap(children: JSX.Element, wrappers: WrapperSet) {
let content = children;
wrappers.forEach((wrapper) => {
const [ComponentType, props] = wrapper;
content = iteration(ComponentType, props, content)
});
return content
}
Playground

Why does passing children affect Typescript errors?

I seem to have encountered an odd scenario where the Typescript compiler is confused by the passing of the children prop to component, resulting in unsafe behaviour.
I want to have a component which can accept subtitle (text) and subtleSubtitle (boolean which affects the style) props ONLY if a title prop is also passed. If a subtitle is passed but no title, this should be a Typescript error.
However, when trying to implement this I found that Typescript seems to allow invalid props to be passed to the component.
Here is the code
import React from 'react'
// subtitle and subtleSubtitle are only permissible when title is present.
type Props =
| {
children: React.ReactNode;
title: string;
subtitle: string;
subtleSubtitle?: boolean
}
| { children: React.ReactNode; title?: string }
// Plain function to test behaviour outside of React components
const myFunc = (props: Props) => {
if (props.title && 'subtitle' in props) {
console.log(props.title + props.subtitle)
return;
}
console.log(props.title)
return;
}
// Component which accepts the same props
const MyComponent: React.FC<Props> = (props) => {
if (props.title && 'subtitle' in props) {
return (<div>
<h1>{props.title}</h1>
<p style={{ color: props.subtleSubtitle ? "grey" : "black" }}>{props.subtitle}</p>
</div>)
}
return <h1>{props.title}</h1>
}
myFunc({ subtitle: "some text" }) // Expect error as subtitle can only be passed if title is passed
myFunc({ subtitle: "some text", children: <p>hi</p> }) // Expected same error but there is none
// same as above, expect an error
const MyParentComponent = () => {
return <MyComponent subtitle="some text" />
}
// Expected error but there is none
const MyOtherParentComponent = () => {
return <MyComponent subtitle="some text">Hello</MyComponent>
}
Playground link:
https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wChSYBPMJOABRzAGc4BeOAbzjQAtgAbACZEAdgC5ExGADpk6GADkIgpAG44MYDH5IJTGFGAiA5uqYBXAEabtuuPsMmzV2wGUXWnQH4JliBB0UETgAXzgAH05uPiFRCTkMWSklFXUbbz0DI2NQ8gB6PPp+FCM4THMRDGAIYJgIDSR9OC04AHdoAGsWCHMYJmAVOAhMSXluXEgRJBE+0jQappBKADEKtDY4AAowRiYJBghmAEo2AD5OUjhmke3d6XTaADJHggtrTyR8ZuCdw6YTjiXK7jERMAJIaT8CDGW5-e4fOAAajgv2Y0jeDyOQKuRBg5igIlUQJC5Cu81B4Mh0NhaMxQNx+MJpBJpAKcAAwhMatN4K0+Lw4Og0EgwH0NDxaEwUCBaKimHMFvAALKUTngbkzeJSaTLdkAHgOzHO7Bp-zOFyuwBucvhtjgz1eHlsX1KcoB2LgDIJWz1gmAADdTh6rnqeABGU4cG0PEJ6vLhoPA4F6sD2Kg6VgcLjzKFQCQ2jE6dzvO1eOAAImMREo5bgEnLlmKaA6tZCIUjBadOljeTAieTeT9gaxVxZOKQeO9oYjUbuMbjCeZ5CWq0qmy4GI+9bBMoaAA8YK2TmyAKJ7mgYOBIKA4KCClibu1oIJDET8ShwSyylBMJhIQTXBoCLACwYA-n+gikCuazrvYXZ2OWO60DASAHuWAA00QCMI0wSCmpx8HGfahMehRnheKEAVKu7Xren69OK160CBcAiNy+SFNRtA-oKfj+kgmGoRRgrBLR0AKqCyqUHQKCiDAaqTDyGybCcrDnIC46TsEeoqgpGrwI+GaIbgyGoYecB5EGLKnueSAYP+V43tA9HwDAEpEM0LBsVMElNCqADybnXjJcl6VMMzKap6n0hOjJwDpqpcuFBnwawxm7ihaGnAAEkg-BQnGulJTyVmkEAA
Its easy to think that Union Types are exclusive, but they are actually a bit different, from Typescript docs:
It might be confusing that a union of types appears to have the intersection of those types’ properties. This is not an accident - the name union comes from type theory. The union number | string is composed by taking the union of the values from each type. Notice that given two sets with corresponding facts about each set, only the intersection of those facts applies to the union of the sets themselves. For example, if we had a room of tall people wearing hats, and another room of Spanish speakers wearing hats, after combining those rooms, the only thing we know about every person is that they must be wearing a hat.
You want your types to be exclusive, so that only one type will be passed without intersection. This can be done by using typescript's Conditional Types:
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U
Now you can:
type Props = XOR<{ children: React.ReactNode; title: string; subtitle: string; subtleSubtitle?: boolean }, { children: React.ReactNode; title?: string }>
myFunc({ subtitle: "some text" }) // Error
myFunc({ subtitle: "some text", children: <p>hi</p> }) // Error
myFunc({ title: "cool", subtitle: "some text", children: <p>hi</p> }) // Works
I think I figured it out. Typescript doesn't prevent you from having additional properties on an object, so something like this compiles just fine:
type Props =
| {
title: string;
anotherProp: boolean;
subtitle: string;
subtleSubtitle?: boolean
}
| { anotherProp: boolean; title?: string }
const x: Props = { anotherProp: true, subtitle: "what" }
This is because { anotherProp: true } is assignable to the { anotherProp: boolean; title?: string } type. The subtitle property is ignored as an "extra" property on the object.
It should be possible to avoid this issue by structuring the union type differently or by using a tagged union (though tagged unions for props of a component is rather unusual and would be a strange DX)

TypeScript error when passing untyped string to React component with typed props?

I have an Icon component:
type Props = {
icon:
| "one"
| "two";
}
const Icon: React.FC<Props> = ({ icon }) => {
// generate icon
}
If I use it in another component with a hard coded value everything is fine:
const MyComponent = () => {
return(
<div>
<Icon icon="one" />
</div>
)
}
However when it's set dynamically from props I get a TypeScript error, even when the icon is displayed correctly:
type Props = {
icon?: string;
};
const MyComponent: React.FC<Props> = ({ icon }) => {
return(
<div>
{
icon && <Icon icon={icon} />
}
</div>
)
}
TS2322: Type 'string' is not assignable to type '"one" | "two".
index.tsx(8, 3): The expected type comes from property 'icon' which is declared here on type 'IntrinsicAttributes & Props & { children?: ReactNode; }'
Is this because TypeScript doesn't know that the string value passed will be either "one" or "two"?
If so then the only solution I can think of would be to export "one" and "two" as an enum. However this would make the component annoying to use as you'd always have to import this enum, rather than just pass the string that you want. Is there a more elegant solution?
Props in component have union values only that is one or two but where you are using the component its data type is defined by optional string variable so it's a mismatch
type Props = {
icon:
| "one"
| "two";
}
type Props = {
icon?: string;
};
To fix it you need to make both of these consistent
type Props = {
icon?: "one"|"two";
};

Resources