Intro
Hi! Over the past year or 2 whilst building multiverseonline.io (the web site for our app, Multiverse) I’ve had to add React to the list-of-stuff-I-kind-of-know. The site is driven by React, using React-Bootstrap on top, and all written in TypeScript. Unfortunately however, in my quest for knowledge on the subject, I’ve often found stack overflow and other sources can be summed up with the phrase:,
“there’s a million opinions on your question, but unless you know the right answer to your question, you can’t tell which is the right opinion“
My issue in this post is a classic example of said questions. There’s plenty of answers, but identifying the correct one is none-trivial!
My Bootstrap Problem
React-Bootstrap has all sorts of handy stuff for creating nice React components, such as the Button
…
<Button variant='primary'>
Some text
</Button>
Giving us results like this (from the Bootstrap documentation):

However, recently I wanted to create a nice reusable version of the light button that had an info icon inside it:

This sort of thing is exactly what React is designed to do, so with a pretty icon (from react-icons) and a tiny bit of code:
<Button variant="light" className="mv-circular-button">
<BsInfo/>
</Button>
Along with some simple CSS:
.mv-circular-button {
position: relative;
height: 40px;
width: 40px;
padding: 0;
border-radius: 50%;
border: none;
box-shadow: none !important;
}
.mv-circular-button svg {
width: 75%;
height: 75%
}
We have a simple button consisting of the following:
- The Bootstrap
Button
with the light variant creates a nicely styled button element. - The
BsInfo
component comes from react-icons and is an SVG with that pretty ‘i’ inside it. - The button CSS makes it a fixed width/height, sets the border to 50% so it becomes circular, and removes the annoying shadow effect Bootstrap adds
- The SVG CSS just enforces a size for the icon
All good so far, but react is all about encapsulation and reuse of components, and I want to use that same info button in multiple places.
A simple functional component
Well the obvious (and I’m told the 2021 way of doing things) is to create a simple functional component like this one:
export const MyInfoButton = () => (
<Button variant="light" className="mv-circular-button">
<BsInfo/>
</Button>
)
It’s in a separate TSX file, and is called MyInfoButton
. Now within my page I simply have to write:
<MyInfoButton/>
And we have a button! Of course a button isn’t much use unless it does something when you click it, so let’s add an onClick event…
<MyInfoButton
onClick={() => console.log("hello")}
/>
As anybody with a bit of TypeScript/React experience will point out, this immediately fails to compile. We haven’t defined any properties at all for the info button yet, so the onClick property is unrecognised. To make our original component work, we need to define and pass in some properties:
type MyInfoButtonProperties = {
onClick?: ()=>void
}
export const MyInfoButton = (props: MyInfoButtonProperties) => (
<Button onClick={props.onClick} variant="light" className="mv-circular-button" >
<BsInfo/>
</Button>
)
The MyInfoButton
component now takes a set of properties defined by MyInfoButtonProperties
. The only property for now is onClick, which is passed directly through to the Button
component.
More properties
Now what if I want to start expanding on this? Maybe I want to be able to add a mouse move event and specify whether the button is disabled. Each of these are properties Button
already supports, so I could just add them to MyInfoButtonProperties
and pass them through as before:
type MyInfoButtonProperties = {
onClick?: ()=>void;
disabled?: boolean;
onMouseMove?: ()=>void;
}
export const MyInfoButton = (props: MyInfoButtonProperties) => (
<Button
onClick={props.onClick}
onMouseMove={props.onMouseMove}
disabled={props.disabled}
variant="light"
className="mv-circular-button" >
<BsInfo/>
</Button>
)
But we’re just duplicating properties of the internal button here, which feels a little messy. To clean things up a little, we can use the ‘…’ syntax to help out:
type MyInfoButtonProperties = {
onClick?: ()=>void;
disabled?: boolean;
onMouseMove?: ()=>void;
}
export const MyInfoButton = (props: MyInfoButtonProperties) => (
<Button {...props}
variant="light"
className="mv-circular-button" >
<BsInfo/>
</Button>
)
This simply passes anything in props through to the internal Button
component. Much cleaner!
What about ALL the properties?
The model we’ve taken thus far is to work out what functionality from the internal Button component we want to expose, and provide properties to do exactly that – no more, and no less. This very constrained model makes perfect sense a lot of the time. Tightly constrained interfaces like this are a great way to keep your code understandable and reduce bugs. However, despite many an angry stack overflow comment, there are exceptions, and this is not ‘always the right thing to do’ (I hate that phrase)!
In this case I’m really just trying to add a small bit of functionality to the existing very powerful button. I specifically want a user of MyInfoButton
to have immediate access to the 100s of properties any normal button has.
For a normal html button element (as apposed to a Bootstrap one), we can use the types supplied by React:
type MySimpleInfoButtonProperties = {
//any custom bits
} & ButtonHTMLAttributes<HTMLButtonElement>
export const MySimpleInfoButton:FunctionComponent<MySimpleInfoButtonProperties> = (props) => (
<button {...props}
className="mv-circular-button" >
<BsInfo/>
</button>
)
Here ButtonHTMLAttributes
contains the list of all properties a standard HTML button can have. The generic HTMLButtonElement
type argument is used in the definition of various event properties such as onClick.
Unfortunately for us, Bootstrap is not this simple!
Bootstrap defines its button like this:
import * as React from 'react';
import { BsPrefixComponent } from './helpers';
export interface ButtonProps {
active?: boolean;
block?: boolean;
variant?:
| 'primary'
| 'secondary'
| 'success'
| 'danger'
| 'warning'
| 'info'
| 'dark'
| 'light'
| 'link'
| 'outline-primary'
| 'outline-secondary'
| 'outline-success'
| 'outline-danger'
| 'outline-warning'
| 'outline-info'
| 'outline-dark'
| 'outline-light';
size?: 'sm' | 'lg';
type?: 'button' | 'reset' | 'submit';
href?: string;
disabled?: boolean;
}
declare class Button<
As extends React.ElementType = 'button'
> extends BsPrefixComponent<As, ButtonProps> {}
export default Button;
Based on the above, the obvious starting point is a new set of properties as follows:
type MyInfoButtonProperties = {
//any custom bits
} & ButtonProps & ButtonHTMLAttributes<HTMLButtonElement>
export const MyInfoButton = (props: MyInfoButtonProperties) => (
<Button {...props}
variant={props.variant || "light"}
className={"mv-circular-button " + (props.className || "")}>
<BsInfo/>
</Button>
)
Here I’ve extended the property type so it can include:
- Any extra properties I might want to add later as my info button gets fancier
- The core HTML attributes that any normal button needs, as per the previous pure react example
- The bootstrap
ButtonProps
which include things like variant, size, disabled etc
I’ve also tweaked the 2 additional properties I pass to the internal button so that:
- The default variant is light
- The class name starts with “mv-circular-button” but can include additional classes passed in
So far we’re all good. My button still works!
The ‘as’ property
All good so far, but we’ve lost something important. Bootstrap supports a property named “as” on all its types, that allows you to override the HTML element used for the button. This functionality varies from pointless to useful to essential with Bootstrap, and when trying to create a generic, reusable info button, it’s not something I want to design out.
The problem is that our properties currently use ButtonHTMLAttributes<HTMLButtonElement>. This isn’t going to work for anything other than a button element!
Starting from the Bootstrap button, we can see it defines a generic type variable called“As”:
declare class Button<
As extends React.ElementType = 'button'
> extends BsPrefixComponent<As, ButtonProps> {}
What we need to do is build the exact set of properties a Bootstrap Button
needs, taking into account the “As” variable, so let’s follow things through. Button
simply extends something called BsPrefixComponent<As,ButtonProps>
. In the Bootstrap helpers file we find:
export class BsPrefixComponent<
As extends React.ElementType,
P = {}
> extends React.Component<ReplaceProps<As, BsPrefixProps<As> & P>> {}
BsPrefixComponent
is a React class component (not a functional component like our info button), and it takes the rather confusing set of properties:
ReplaceProps<As, BsPrefixProps<As> & P>
In the context of our button, which defaults “As” to be ‘button’, and passes through ButtonProps
as P
, this resolves to:
ReplaceProps<'button', BsPrefixProps<'button'> & ButtonProps>
BsPrefixProps
isn’t that interesting:
export interface BsPrefixProps<As extends React.ElementType> {
as?: As;
bsPrefix?: string;
}
It defines the “as” property and something called bsPrefix, so we’re creating a component that uses these 2 properties plus any in ButtonProps
. But where do the 100s of other useful HTML ones come from? Well let’s look at the helpfully named ReplaceProps
:
export type Omit<T, U> = Pick<T, Exclude<keyof T, keyof U>>;
export type ReplaceProps<Inner extends React.ElementType, P> = Omit<
React.ComponentPropsWithRef<Inner>,
P
> &
P;
Note: ES5 for TypeScript defines its own version of Omit, which is different to the bootstrap version!
The “Inner” type variable is passed in from the “As” type variable which in our case defaults to ‘button’. Omit removes all the fields in one type that exist in another. In this case we’re:
- Taking all the fields of the defined by
React.ComponentPropsWithRef<'button'>
- Removing any that are defined in P, which is currently
BsPrefixProps<As,ButtonProps>
- Then adding back all those that were removed!
So why do this? Well the important detail is that the removal doesn’t care about the types of fields – it simply deals with their names. Here Bootstrap is stripping out of the React type any fields defined in P, and replacing them with the versions from P.
For a simpler example of this type stripping, take the following code:
type TestType1 = {
name: string;
age: string;
}
type TestType2 = {
age: number;
address: string;
}
type CombinedType = TestType1 & TestType2;
let x: CombinedType = {
name: "hello",
age: 10,
address: "somewhere"
};
Here we create 2 types, both of which define an ‘age’ field, but the first defines it as a string, and the second defines it as number. Because they don’t agree, CombinedType
will end up with:
type CombinedType = {
name: string;
age: never;
address: string;
}
The ‘age’ field has been assigned the type ‘never’, as it can never be both a number and a string. The result is that the final line in which we set age to 10 will result in the following TypeScript error:
Type ‘number’ is not assignable to type ‘never’.
The bootstrap technique uses Omit
as follows:
type TestType1 = {
name: string;
age: string;
}
type TestType2 = {
age: number;
address: string;
}
/*
type TestType1WithoutAgeField = {
name: string
}
*/
type TestType1WithoutAgeField = Omit<TestType1,TestType2>
/*
type CombinedType = {
name: string;
age: number;
address: string;
}
*/
type CombinedType = TestType1WithoutAgeField & TestType2;
Long story short, Bootstrap’s type magic is giving us a new type with:
- All those from P
- 2 fields called ‘as’ and ‘bsPrefix’ from
BsPrefixProps
- Any remaining fields that the normal HTML element would have
In the case of Button this is:
- The fields from
ButtonProps
- ‘as’ and ‘bsPrefix’ from
BsPrefixProps
- By default, any additional fields a button HTML element has
You might wonder how the text ‘button’ gets converted to “all the fields a button element would have”. If you follow through the react code for React.ComponentPropsWithRef
what you’ll find is that ‘button’ is used as a key of the JSX.IntrinsicElements
interface, defined as follows:
interface IntrinsicElements {
// ... lots of fields ...
button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
// ... lots more fields ...
}
}
With all this work, we can say that Button
is a class component that takes the properties:
ReplaceProps<As, BsPrefixProps<As> & ButtonProps>
Where ‘As’ defaults to ‘button’.
From there, it’s a short step to conclude that the properties for the extended info button should be as follows:
type NewButtonProps = {
//any new properties we want to add go here
} & ButtonProps
type MyInfoButtonProperties<As extends React.ElementType = 'button'> =
ReplaceProps<As, BsPrefixProps<As> & NewButtonProps>
Creating the class component
So we’ve setup a new simple type that contains any new bits we want to add, plus the basic properties defined for the Bootstrap button. Then used the ReplaceProps
type magic to add in any extras from the core HTML element. As with the rest of Bootstrap, the new MyInfoButtonProperties
takes an ‘As’ type argument that can be used to override the element it represents in the DOM.
Before diving back into function world, let’s copy the Bootstrap pattern of a fully fledged class component
export class MyInfoButton<As extends React.ElementType = 'button'>
extends React.Component<MyInfoButtonProperties<As>>
{
render = () => {
return (
<Button {...this.props}
variant={this.props.variant || "light"}
className={"mv-circular-button " + (this.props.className || "")}>
<BsInfo/>
</Button>
)
}
}
Armed with this new component, we can now go back to the original page and do something along the lines of…
<MyInfoButton
as='a'
target="bla"
onClick={() => console.log("hello")}
/>
Note how the ‘as’ parameter is now available to us. Looking at it in the browser, you can see React is inserting an anchor (‘a’) element into the DOM instead of a button element:

Back to a functional component
Wonderful! We could stop there, but all the cool React kids say functional components are where it’s at and I’m not one to ignore peer pressure. The syntax for an arrow function definition is a little ugly but not too complex:
export const MyInfoButton = <As extends React.ElementType = 'button'>(props: MyInfoButtonProperties<As>) => (
<Button {...props}
variant={props.variant || "light"}
className={"mv-circular-button " + (props.className || "")}>
<BsInfo/>
</Button>
)
Or as a standard javascript function (which I personally find a little easier on the eyes):
export function MyInfoButton<As extends React.ElementType = 'button'>(props: MyInfoButtonProperties<As>) {
return <Button {...props}
variant={props.variant || "light"}
className={"mv-circular-button " + (props.className || "")}>
<BsInfo/>
</Button>
}
Putting it all together (with a handy little type for wrapping the bootstrap bits together):
//imports
import React from "react";
import { Button, ButtonProps } from "react-bootstrap";
import { BsPrefixProps, ReplaceProps } from "react-bootstrap/helpers"
import { BsInfo } from 'react-icons/bs'
import './circular-button.css'
//handy type defintion to wrap up the replace+bsprefix bits of bootstrap
type BootstrapComponentProps<As extends React.ElementType, P> = ReplaceProps<As, BsPrefixProps<As> & P>
//our extended button properties
type NewButtonProps = {
//any new properties we want to add go here
} & ButtonProps
//boot-strap-ified full button properties with all the bells and whistles
type MyInfoButtonProperties<As extends React.ElementType = 'button'> =
BootstrapComponentProps<As, NewButtonProps>
//our button!
export function MyInfoButton<As extends React.ElementType = 'button'>(props: MyInfoButtonProperties<As>) {
return <Button {...props}
variant={props.variant || "light"}
className={"mv-circular-button " + (props.className || "")}>
<BsInfo/>
</Button>
}
Summary
An interesting little problem, and maybe a convincing answer to it, should anybody have the same question as me!
As I mentioned earlier, this kind of expose it all technique shouldn’t necessarily be avoided, but it should be approached with some thought. By blindly exposing everything in the new button type we may well have exposed some functionality that doesn’t quite work anymore due to our adjustments. This could clearly cause some confusion, but on the plus side we’ve ended up with something extremely flexible without lots of code to maintain.
Anyhoo, that’s it for my first React/TypeScript post. Stay tuned for more posts on this or other random things!