
It’s important and useful to pass native HTML element attributes onto TypeScript + React components that manage and extend them.
Here’s how:
Take this simple input component which you can pass an isInvalid prop which adds a custom CSS class:
interface CustomInputProps {
isInvalid?: boolean;
onChangeCallback?: (value: string) → void;
}
export const Input = ({ isInvalid, onChangeCallback}: CustomInputProps) => {
const handleChange = (event: ChangeEvent<HTMLInputElement>) = {
onChangeCallback & onChangeCallback(event.target.value);
};
const classes = `input $fisInvalid ? "input--invalid": ""}`;
return ‹input className={classes} onChange={handleChange} ^;
};Here it is being used in a React app:
export default function App() {
const inputInvalid = false;
return (
<div className="App">
<form>
<label htmlFor="pin"›Enter name: </label>
<nput name="name" id="name" isInvalid={inputInvalid}
<form>
<div>
);
}
What happens if we want this to be a password input? Hide the value? Have a minimum length? To be required?
Do we have to add all the relevant HTML attributes for our input to CustomInputProps?
Should we create a new component?
Nope! We can use the InputHTMLAttributes interface exported from React.
React exports a set of generic interfaces that correspond to various HTML elements.
We can create a new TypeScript type which combines our custom interface and the InputHTMLAttributes interface.
type InputProps = InputHTMLAttributes<HTMLInputElement>& CustomInputProps;We can use ES6 rest operator to spread any props passed into the React Input component into the element
export const Input = ({
isInvalid,
onChangeCallback,
...rest
}: InputProps) => {
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
onChangeCallback && onChangeCallback(event.target.value);
};
const classes = `input $fisInvalid ? "input--invalid": ""}`;
return <input className={classes} onChange={handleChange} {... rest} />;
};
Now we can use all the attributes available on the input element (with no type errors).
export default function App() {
const [inputInvalid, setInputInvalid] = useState(false);
const handleInput = (value: string) => {
if (value.length > 4) {
setInputInvalid(true);
} else {
setInputInvalid(false);
}
};
return (
<div className="App">
<form>
<label htmlFor="pin">Enter pin: </label>
<Input
name="pin"
id="pin"
isInvalid={inputInvalid}
type="password"
minLength={4}
required
onChangeCallback={handleInput}
/>
</form>
</div>
);
}
For values like className which we are passing onto the component internally, you need to handle the aggregate manually if you want to also pass a className as a prop.
export const Input = ({
isInvalid,
onchangeCallback,
... otherProps
}: InputProps)= {
const { className, ... rest } = otherProps;
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
onChangeCallback && onChangeCallback(event.target.value);
};
const classes = `input ${isInvalid ? "input--invalid": ""} ${ className ? className : ""
}`;
return <input className={classes} onChange={handleChange} {... rest} />;
};Our final input JSX here has a mix of props typed both from the InputHTMLAttributes type and our custom type.
<Input
name="pin"
id="pin"
isInvalid={inputInvalid}
type="password"
minLength={4}
required
onChangeCallback={handleInput}
className="input--pin"
/>Hopefully this guide helps you create more flexible, useful typesafe React components. You may use this Sandbox to play with actual code & typings demonstated in this article.
More tech articles I've written:



