Chapters - Table of contents ā
Let's combine all the knowledge on advanced and re-usable types by looking at a custom <Button> component built on top of material-ui's <Button> component.
Our UIKit exposes a <Button> component that provides the following props:
- disabled?: boolean
- href?: string
- size?: 'small' | 'medium' | 'large'
- loading?:boolean and loadingPlaceholder?: string
- align?: 'left' | 'center' | 'right'
- leftIcon and rightIcon (more on this later)
Here are some usage examples:
1import React from "react";
2import "./styles.css";
3
4import { Button } from "./Button";
5
6export default function App() {
7 return (
8 <div className="App">
9 <h1>Custom Button</h1>
10 <p>
11 <h2>Standard button</h2>
12 <Button>{"Click me"}</Button>
13 </p>
14 <p>
15 <h2>Loading button</h2>
16 <Button loading loadingPlaceholder="I'm loading!">
17 {"Click me"}
18 </Button>
19 </p>
20 <p>
21 <h2>Async button</h2>
22 <Button
23 onClick={() => new Promise((resolve) => setTimeout(resolve, 1000))}
24 >
25 {"Click me"}
26 </Button>
27 </p>
28 </div>
29 );
30}
31
The full code of <Button> is accessible here:
Take a few minutes to analyze the code before continuing to the section below.
<Button> anatomy
Let's take a look at some specific parts of <Button>'s typings.
ButtonProps
Partially extending material-ui's ButtonProps
Since our <Button> component is a wrapper around material-ui's <Button> component, ButtonProps is naturally extending a sub-set interface of material-ui's ButtonProps using Pick<T, K>.
This allows us to keep our custom <Button> component in sync with material-ui's <Button> component.
The case of rightIcon and leftIcon
Both props share the same base ButtonIcon type, however, only the rightIcon can receive a callback.
For this reason, rightIcon is composing ButtonIcon and ButtonIconActionable using the & operator.
Such type could have been built using an interface with extends, however, most of the time, we would prefer a shorter version leveraging inlines & or |.
Promisify<T> custom type helper
To avoid code duplication, our <Button> is shipped with a Promisify<T> type as follows:
1export type Promisify<T extends (...args: any) => any> = () => Promise<
2 ReturnType<T>
3>;
4
Such type allows us to write
1onClick?: ButtonCallback | Promisify<ButtonCallback>;
2
instead of:
1onClick?: () => void | () => Promise<void>;
2
Making it easier to maintain when ButtonCallback will change.
Conclusion
Our
<Button> definition is a good example of leveraging TypeScript advanced types in order to build re-usable and maintainable types by:
- extending external types instead of rewriting them: ButtonProps
- extracting reused types: ButtonAlign, ButtonIcon, ButtonCallback
- leveraging generics to extract common transformations: Promisify<T>