Introduction
These notes should help in better understanding TypeScript
and might be helpful when needing to lookup up how to leverage TypeScript in a specific situation. All examples are based on TypeScript 3.2.
Pick and Exclude
These notes focus on typing higher order components in React, but it's interesting to understand omit
and Exclude
as we will need both functions to handle the different higher order component (hoc) implementations. With Pick
we can, as the name implies, pick specific keys from a provided type definition. For example we might be using an object spread and want to select specific properties and spreading the rest. Let's take a look at the following example to get a better idea:
const { name, ...rest } = props;
We might want to do something with name inside a function but only pass on the rest props.
type ExtractName = {
name: string
}
function removeName(props) {
const {name, ...rest} = props;
// do something with name...
return rest:
}
Let's add types to removeName
function.
function removeName<Props extends ExtractName>(
props: Props
): Pick<Props, Exclude<keyof Props, keyof ExtractName>> {
const { name, ...rest } = props;
// do something with name...
return rest;
}
There is a lot going on here, first we extended our generic Props
to include the name property.
Then we extracted the name
property and returned the rest properties. To tell TypeScript how our generic rest types are structured we need to remove all ExtractName properties (name in this specific case). This is what Pick<Props, Exclude<keyof Props, keyof ExtractName>>
does. Let's break this further down, to be getter a better understanding. Exclude
removes specific keys:
type User = {
id: number;
name: string;
location: string;
registeredAt: Date;
};
Exclude<User, "id" | "registeredAt"> // removes id and registeredAt
We can achieve the same thing with Pick
:
Pick<User, "name" | "location">
We can rewrite our above definition:
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type Diff<T, K> = Omit<T, keyof K>;
Now that we have a Diff
function we can rewrite our removeName
function:
function removeName<Props extends ExtractName>(
props: Props
): Diff<Props, ExtractName> {
const { name, ...rest } = props;
// do something with name...
return rest;
}
We should have a basic understanding of how Pick
and Exclude
function and also added Omit
and Diff
that we will use when will type hocs in the following section.
Higher Order Components
We will consult the official React docs for better understanding some conventions and then type the different hoc variants.
There is an important convention that we need to consider: Pass Unrelated Props Through to the Wrapped Component (see docs).
Our first example is based on an example from the docs, where we want to log props by providing a component that logs a wrapped component.
function withLogProps(WrappedComponent) {
return class LogProps extends React.Component {
componentWillReceiveProps(nextProps) {
console.log("Currently available props: ", this.props);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
We can leverage React.ComponentType
which is a React specific type that will allow us to pass in a component class or function as a wrapped component. As we're not extending or narrowing any props in our withLogProps
higher order component, we can pass the generic props through.
function withLogProps<Props>(WrappedComponent: React.ComponentType<Props>) {
return class LogProps extends React.Component<Props> {
componentWillReceiveProps() {
console.log("Currently available props: ", this.props);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
Next, let's see how we can type a higher order component that expects additional props to show a message when an error has occurred.
function withErrorMessage(WrappedComponent) {
return function() {
const { error, ...rest } = props;
return (
<React.Fragment>
<WrappedComponent {...rest} />
{error && <div>{error}</div>}
</React.Fragment>
);
};
}
The withErrorMessage
looks similar to the initial example we built.
function withErrorMessage<Props>(WrappedComponent: React.ComponentType<Props>) {
return function(props: Props & ErrorLogProps) {
const { error, ...rest } = props as ErrorLogProps;
return (
<React.Fragment>
<WrappedComponent {...rest as Props} />
{error && <div>{error}</div>}
</React.Fragment>
);
};
}
There are some interesting aspects here, that we need to clarify.
Our hoc expands the expected props by expecting an error
aside from all the props expected from the wrapped component, this can be achieved by combining the generic wrapped component props with the required error message prop: Props & ErrorLogProps
.
The other interesting aspect is that we need to explicitly define which props are ErrorLogProps
by typecasting our destructured props:const { error, ...rest } = props as ErrorLogProps
TypeScript will still complain when passing through the rest props, so we need to typecast the rest props as well: <WrappedComponent {...rest as Props} />
. This might change in the future, but of 3.2, this is needed to prevent TypeScript from complaining.
There are situations where we want to provide specific functionalities and values to a wrapped component as well as preventing these functions and values being overridden by provided props.
Our next higher order component should narrow down the API.
Let's assume we have an Input
component the expects
const Input = ({ value, onChange, className }) => (
<input className={className} value={value} onChange={onChange} />
);
The higher order component should provide the value
and onChange
properties.
function withOnChange(WrappedComponent) {
return class OnChange extends React.Component {
state = {
value: "";
};
onChange = e => {
const target = e.target;
const value = target.checked ? target.checked : target.value;
this.setState({ value });
};
render() {
return (
<WrappedComponent
{...this.props}
onChange={this.onChange}
value={this.state.value}
/>
);
}
};
}
Let's define the needed prop types first.
type InputProps = {
name: string,
type: string
};
type WithOnChangeProps = {
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
value: string | boolean
};
This means we can define our Input
component by combining these prop type definitions.
const Input = ({
value,
onChange,
type,
name
}: InputProps & WithOnChangeProps) => (
<input type={type} name={name} value={value} onChange={onChange} />
);
Adding types to the withOnChange
component, we can apply everything we have learned so far.
type WithOnChangeState = {
value: string | boolean;
};
function withOnChange<Props>(WrappedComponent: React.ComponentType<Props>) {
return class OnChange extends React.Component<
Diff<Props, WithOnChangeProps>,
WithOnChangeState
> {
state = {
value: ""
};
onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const target = event.target;
const value = target.type === "checkbox" ? target.checked : target.value;
this.setState({ value });
};
render() {
return (
<WrappedComponent
{...this.props as Props} // we need to be explicit here
onChange={this.onChange}
value={this.state.value}
/>
);
}
};
}
By using our previously defined Diff
type we can extract all keys that we want to prevent from being overridden. This enables us to provide our Input
component with the onChange
and value
properties.
const EnhancedInput = withOnChange(Input);
// JSX
<EnhancedInput type="text" name="name" />;
There are situations where we need to expand the props, f.e. we would like to enable developers using withOnChange
to provide an initial value. We can rewrite our component by enabling to provide an initialValue
property.
type ExpandedOnChangeProps = {
initialValue: string | boolean;
};
function withOnChange<Props>(WrappedComponent: React.ComponentType<Props>) {
return class OnChange extends React.Component<
Diff<Props, WithOnChangeProps> & ExpandedOnChangeProps,
WithOnChangeState
> {
state = {
value: this.props.initialValue
};
onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const target = event.target;
const value = target.type === "checkbox" ? target.checked : target.value;
this.setState({ value });
};
render() {
const { initialValue, ...props } = this.props as ExpandedOnChangeProps;
return (
<WrappedComponent
{...props as Props} // we need to be explicit here
onChange={this.onChange}
value={this.state.value}
/>
);
}
};
}
There are two interesting things to note here. We expanded the OnChange
class props by defining Diff<Props, WithOnChangeProps> & ExpandedOnChangeProps
, the other important is that we have to remove initialValue
from the props passed down to our wrapped component. We have seen this done in our initial example, by spreading the generic props and removing the initialValue
:
const { initialValue, ...props } = this.props as ExpandedOnChangeProps;
Another possible case, where we might want to provide a higher order component is when we want to define a generic component, that expects a wrapped component as well as additional configurations or functionalities. Let's write a component that expects a fetch function and a component and returns a component that depending on the result of the fetch either displays nothing, a loading indicator, an error message or in case of a successful fetch the wrapped component.
function withFetch(fetchFn, WrappedComponent) {
return class Fetch extends React.Component {
state = {
data: { type: "NotLoaded" }
};
componentDidMount() {
this.setState({ data: { type: "Loading" } });
fetchFn()
.then(data =>
this.setState({
data: { type: "Success", data }
})
)
.catch(error =>
this.setState({
data: { type: "Error", error }
})
);
}
render() {
const { data } = this.state;
switch (data.type) {
case "NotLoaded":
return <div />;
case "Loading":
return <div>Loading...</div>;
case "Error":
return <div>{data.error}</div>;
case "Success":
return <WrappedComponent {...this.props} data={data.data} />;
}
}
};
}
There's some work we need to do to prevent TypeScript from complaining.
The first thing we can do is define the actual component state:
type RemoteData<Error, Data> =
| { type: "NotLoaded" }
| { type: "Loading" }
| { type: "Error", error: Error }
| { type: "Success", data: Data };
type FetchState<Error, Data> = {
data: RemoteData<Error, Data>
};
We can define the promise result type that our withFetch
component should expect when calling the provided function, that way we can guarantee that the returned promise result type matches the expected data property in our wrapped component.
function withFetch<FetchResultType, Props extends { data: FetchResultType }>(
fetchFn: () => Promise<FetchResultType>,
WrappedComponent: React.ComponentType<Props>
) {
return class Fetch extends React.Component<
Omit<Props, "data">,
FetchState<string, FetchResultType>
> {
state: FetchState<string, FetchResultType> = {
data: { type: "NotLoaded" }
};
componentDidMount() {
this.setState({ data: { type: "Loading" } });
fetchFn()
.then(data =>
this.setState({
data: { type: "Success", data }
})
)
.catch(error =>
this.setState({
data: { type: "Error", error }
})
);
}
render() {
const { data } = this.state;
switch (data.type) {
case "NotLoaded":
return <div />;
case "Loading":
return <div>Loading...</div>;
case "Error":
return <div>{data.error}</div>;
case "Success":
return <WrappedComponent {...this.props as Props} data={data.data} />;
}
}
};
}
There are more examples we can write, but as introductory into the topic, these examples should be a building block to study the topic further.
If you have any questions or feedback please leave a comment here or connect via Twitter: A. Sharif
Top comments (3)
Thanks
Thanks, Man for sharing this.
Thank you for helpful post.
Btw this example, in the beginning, may confuse
Exclude<User, "id" | "registeredAt">
Cause User not casted to keysof