Building Flexible React Components with TypeScript: Conditional Props in Action
Creating conditional props in TypeScript for versatile React components.
Table of Contents
- What You’ll Learn
- Where You Can Find the Code
- Why Conditional Props Matter
- What We’re Building for Demonstration
- Creating Flexible Types from Interface Variants
- The Input Component Structure
- Ensuring Proper Component Instantiation
What You’ll Learn
- How to create flexible, type-safe components that intuitively indicate required and conflicting props based on variant usage.
- How to extend interfaces to create specialized props for different component variants.
- How conditional props improve component clarity and usability.
Where You Can Find the Code
If you’d like to follow along with a live example on your local machine, you can check out the repo I created for this walkthrough.
Why Conditional Props Matter
Conditional props can help you create flexible components maintaining strict adherence to usage guidelines.
With conditional props, we ensure clarity for fellow developers utilizing our components so they know which props are required based on the intended variant, enhancing component usability and developer experience.
What We’re Building for Demonstration
We’re building a controlled input component with two distinct variations.
One variation includes a character counter for user feedback on input length, while the other presents a static message string. These variants are mutually exclusive, ensuring developers understand the specific requirements for each use case.
When implementing the character counter variant, developers are prompted to set a maximum character count but cannot include a message string. Conversely, when incorporating the message string variant, developers should not include a character counter or specify a maximum character count for the input.
Creating Flexible Types from Interface Variants
import { ChangeEventHandler, useState } from "react";
interface CommonProps {
inputId: string;
labelText: string;
placeholderText: string;
inputValue: string;
onValueChange: ChangeEventHandler<HTMLInputElement>;
}
interface InputWithCharacterCounterProps extends CommonProps {
characterCounter: true;
maxLength: number;
inputMessage?: never;
}
interface InputWithMessageProps extends CommonProps {
inputMessage: string;
characterCounter?: never;
maxLength?: never;
}
type InputProps = InputWithCharacterCounterProps | InputWithMessageProps;
Above, we are defining what the most common props are for every use case of this component, as well as the types that are required or conflicting based on the variant of the component.
This essentially means that every time you use this component, it must have and inputId
, labelText
, placeholderText
, inputValue
, and onValueChange
.
But because the CommonProps
are not included in the InputProps
options, you can’t complete the component with only these props.
To utilize the InputWithCharacterCounterProps
interface, you must declare it with either a characterCounter
, which mandates the inclusion of a maxLength
, or with an inputMessage
.
One thing that you might notice is that both of the extended interfaces include the same props with different types. This is because if we don’t include them, when we try to use those prop values within the component, certain props will not be present on certain types, giving you the added work of defining props on a case by case basis instead of all at once (you’ll see what I mean soon).
A Closer Look at the Extended Interfaces
interface InputWithCharacterCounterProps extends CommonProps {
characterCounter: true;
maxLength: number;
inputMessage?: never;
}
The InputWithCharacterCounterProps
interface defines the properties necessary for an input component featuring a character counter.
If characterCounter
is specified, it signifies the intention to include a character count, necessitating the presence of maxLength
to set the maximum character limit.
Conversely, if maxLength
is provided, it implies the presence of a character counter. So both characterCounter
and maxLength
become required when either one is present. The inputMessage
property is made optional and set to never
to alleviate the burden of assigning an arbitrary value.
interface InputWithMessageProps extends CommonProps {
inputMessage: string;
characterCounter?: never;
maxLength?: never;
}
Here InputWithMessageProps
defines the single property necessary for an input with a message. If there is an inputMessage
prop, characterCounter
and maxLength
are never to be present. Again, these two props are optional so that developers don’t need to assign an arbitrary value when using it.
The Input Component Structure
const Input = (props: InputProps) => {
const {
inputId,
labelText,
placeholderText,
maxLength,
inputValue,
characterCounter,
inputMessage,
onValueChange,
} = props;
return (
<div className='input'>
<label htmlFor={inputId}>{labelText}</label>
<input
type='text'
id={inputId}
placeholder={placeholderText}
maxLength={maxLength}
onChange={onValueChange}
/>
<span>
{characterCounter
? `${inputValue.length}/${maxLength} characters`
: inputMessage}
</span>
</div>
);
};
Ensuring Proper Component Instantiation
function App() {
const [inputValue1, setInputValue1] = useState('');
const [inputValue2, setInputValue2] = useState('');
return (
<div className='flex'>
<Input
inputId='input-1'
labelText='I am Input 1'
placeholderText='Enter something'
inputValue={inputValue1}
onValueChange={e => setInputValue1(e.target.value)}
inputMessage='hello there'
/>
<Input
inputId='input-2'
labelText='I am Input 2'
placeholderText='Enter something'
inputValue={inputValue2}
onValueChange={e => setInputValue2(e.target.value)}
characterCounter
maxLength={100}
/>
</div>
);
}
export default App;
Here we have two successful instances of the Input
component.
If we attempt to remove inputMessage
in the first instance or characterCounter
or maxLength
from the second, we instantly get feedback on incomplete implementation.
This immediate feedback helps fellow developers easily identify missing or conflicting props, ensuring adherence to the component’s expected usage and preventing potential errors in implementation. And everyone loves an improved development experience.
I hope this helps. Thanks for reading!