Here is the
ESLint configuration
I’m using. TypeScript (TS) is about actually putting types so the use of any,
missing parameter types and missing return function types are forbidden.
I’m using Create React App, with the following packages:
"react": "^16.8.6",
"@types/react": "16.8.10",
"typescript": "^3.4.1",
Function component, without logic nor props
This is the simplest case. Our component is a function that doesn’t take anything and only returns a block of JSX.
const Component = (): JSX.Element => (
<p>Hello, World!</p>
);
The only type annotation we need is the return type of the function:
JSX.Element, which is enforced by TS and doesn’t require any import (as
long as the project is configured to use TS, of course).
Function component, without logic
This case is very similar to the previous one. Our component still does not implement any logic, but it takes props as parameters.
interface Props {
message: string;
}
const Component = (props: Props): JSX.Element => (
<p>{props.message}</p>
);
We now also have to annotate our props. The keyword type might be used
instead of the interface one. However, I usually use it for type composition
only.
In case of a short type, the annotation may be inline:
const Component = (props: { message: string }): JSX.Element => (
...
The props may also be deconstructed:
const Component = ({ message }: { message: string }): JSX.Element => (
...
React.FunctionComponent
React.FunctionComponent and React.FC are types describing a function
component. One could do:
interface Props {
message: string;
}
// Or React.FC
const Component: React.FunctionComponent<Props> = ({ message }: Props): JSX.Element => (
<p>{ message }</p>
);
Because the configuration I use requires to annotate all the parameters and return values, the type signature is now duplicated. Anyway, I think it’s better to put the annotations on the left and let the compiler deduce the type on the right hand side. Moreover, these type names may change in the future so I will avoid tying my code to them.
The names React.SFC (Stateless Function(al ?) Component) and
React.Stateless are now deprecated and should not be used anymore.
See some discussion
about it and
the associated pull request.
Going stateful
We now have a state:
interface Props {
message: string;
}
interface State {
selected: boolean;
}
class Component extends React.Component<Props, State> {
state = {
selected: false
};
handleClick = () => {
const { selected } = this.state;
this.setState({
selected: !selected,
});
};
render = () => {
const { handleClick } = this;
const { message } = this.props;
const { selected } = this.state;
return (
<Button onClick={handleClick} type={selected ? 'primary' : 'default'}>
{ message }
</Button>
);
}
}
The state is annotated just like the props and the props are annotated as before.
I’m taking advantage of the
class field proposal to avoid
writing a constructor and still set my initial state. Also, writing methods
as arrow functions allows not to introduce a new this therefore avoiding
the .bind() problem.
If the class component is stateless, React.Component might have only
one type parameter:
class Component extends React.Component<Props> {
...
Dealing with Higher Order Components
An Higher Order Component (HOC) is all about injecting props into a component so we will have to add a few annotations. Here is an example with React Router:
import { withRouter, RouteComponentProps } from 'react-router-dom';
interface Props {
message: string;
}
interface State {
selected: string;
}
type AllProps = Props & RouteComponentProps;
class Component extends React.Component<AllProps, State> {
state = {
selected: false
};
handleClick = () => {
const { selected } = this.state;
this.setState({
selected: !selected,
});
};
render = () => {
const { handleClick } = this;
const { message, location } = this.props;
const { selected } = this.state;
return (
<Button
onClick={handleClick}
type={selected ? 'primary' : 'default'}
// Disable the button if the current URL includes 'banned'
// (This is just an example...)
disabled={!location.pathname.includes('banned')}
>
{ message }
</Button>
);
}
}
export default withRouter(Component);
Juste like in JavaScript (JS), we wrap our component definition with
withRouter. Then, we use type composition in order to create a new AllProps
type which is our Props and the ones exported by React Router. This
allows us to access this.props.location without any problem.
Sometimes, the exported props type requires a type parameter to work properly. For example:
import { withRouter, RouteComponentProps } from 'react-router-dom';
interface MatchParams {
page: string;
}
type Props = RouteComponentProps<MatchParams>;
class ToBeValidated extends Component<Props, ToBeValidatedState> {
...
}
export default withRouter(Component);
Now we can access this.props.match.params.page.
Let’s conclude with a few more or less related resources: