Typescript & React: Manipulating Prop Types

How to use Extend and Pick helpers with generic types, intersection types, and more

Ross Bulat
Apr 27 · 8 min read

This talk tackles how we can handle React prop manipulation in a type-safe way, unveiling some of the more advanced features of Typescript and ensuring that your components — whether ordinary or extended via higher-order components — are fully type safe. Specifically, we will explore:

  • Conventional syntax for extending and manipulating props and prop types, giving you more modularity and flexibility typing React components
  • Visiting a means of removing unwanted props via utility functions and HOCs, utilising the Exclude and Pick helper types to remove type properties we don’t require. As well as conforming types to your props, this also ensures that already-defined props are not overwritten as they travel down your component tree
  • How generics are used to abstract and manipulate prop types

By the end of this talk you will be equipped with the tools for writing scalable and type-safe prop types within your React projects. Let’s start by reviewing how to format and extend prop types.

Extending prop types

The ability to extend prop types is critical for React components. As projects grow there will be a range of components that utilise an identical subset of props. For example, UI components will inevitably be passing className and style, whereas components housing form elements will be passing props like value, error, or isSubmitted. Instead of repeating type definitions for these patters, we can instead abstract them into different types, and combine them where necessary:

type CombinedProps = BaseProps & FormProps

A common solution is to adopt a BaseProps naming convention for your most fundamental types. ExtendedProps can come in many forms throughout your project depending on the type of component.

This is particularly useful with HOCs, that can be seen as extensions of another function. A wrapped component’s types will therefore also be extended or modified in some way to adhere to the HOC prop manipulation.

Note: Base props (I have also came across BareProps in projects) act as the “original” props our wrapped component requires, and will be the type that ExtendedProps will extend later on.

Let’s take the following type as our base props:

type BaseProps = {
className?: string,
style?: React.CSSProperties
};

Our BaseProps type define optional className and style properties, assuming that our components will render some UI. The className is simply of type string, whereas style takes an array of CSS properties — React.CSSProperties providing strong typing for all CSS attributes.

These properties are common, and therefore vital to define, but we also require a way to extend them. Let’s look at at intersection types — a method allowing us to do just this in Typescript.

Intersection Types

Intersection types in Typescript are types that combine properties of two or more types together. We can either attach two types via their name, or simply attach a list of properties in a block to a type:

ExtendedProps = {
isDisabled?: boolean,
isError?: boolean,
...
}
// combining two typestype FullProps = BaseProps & ExtendedProps;
// or attaching properties to a type
type FullProps = BaseProps & {
isDisabled?: boolean,
isError?: boolean,
...
}

Perhaps ExtendedProps here will be attached to form-based components, allowing us to track whether a form element is disabled, has an error, etc. This combined with BareProps give us a means of also styling form components without declaring more types.

If both BareProps and ExtendedProps contain a property of the same name, our project will not compile:

// this is an error!ExtendedProps = {
className?: string,
isDisabled?: boolean,
isError?: boolean,
...
}
type FullProps = BaseProps & ExtendedProps;

This is fairly simple to avoid if your editor is providing real time syntax validation, but also highlights the necessity of removing any unwanted properties from types where they are not needed. We will explore how to do this further down.

We could further extend FullProps by defining function signatures via another type:

// form handler type, expecting values[] and errors[]type FormHandlerProps = {
(values: any[], errors?: string[]) => void
}
type FullProps = BaseProps & ExtendedProps & FormHandlerProps

In the above example, FormHandlerProps expects a function prop that has two parameters and not to return anything. This signature could be used for an form submission handler, onSubmit(), for example.

Note: We can also utilise intersection types for interfaces too. Although there are differences between type and interface, this is out of the scope of this article. This Stack Overflow post covers the differences well.

A note on union types

Intersection types are not to be confused with union types, which provide an either/or scenario. For example, if we knew our component extended a form element, but did not know which form element it would be, we could utilise union types:

type FormElementProps = TextareaProps | DropdownProps;

Similarly, if we wanted a property to be a particular type or null, we can utilise union types to do so. This is not too useful applied to a type property, as we can just used the ? optional declaration for optional properties in place of null. Instead, null unions can be more useful for function parameters and return types:

// getValue can support a key string or index, and can return empty resultsfunction getValue(key: string | number): string | null {     
...
}

Intersection and union types used together provide us with the necessary functionality for extending functions, and are particularly useful for components that adhere to certain type patterns where props will inevitably to added or manipulated through the component tree.

However, the opposite is also true — we may need to remove props from an object before passing them into a wrapped or child component. Let’s explore how to do this using the Exclude and Pick helper types in Typescript alongside extends and keyof type operators.

Removing unneeded props

For whatever reason in your projects, you will come across a time where props will need to be removed from an object. This usually comes in two forms: a utility function or within a higher order component.

A utility function may be wanting to remove a particular prop or a subset of props that are no longer needed. If we were to use a non-Typescript solution, that function may look like the following:

// a function for dropping a subset of propsfunction dropProps(obj) {
let { x, y, z, ...rest } = obj;
return rest;
}

The above function takes obj and removes the x, y and z props from it, returning the resulting rest object. This is useful, and the syntax is clearly showing what we are doing; the spread operator is assigning every prop apart from x, y, and z to rest inside an obj destructure, which is then returned.

However, to be useful with Typescript, this function needs to be generic and needs to be typed. Let’s see how we can do this.

To invent a real-world scenario — let’s say we would like to drop some metadata from obj that is not useful for our component: we would like to remove the _id, created and last_updated fields from obj.

Fields like these will likely leak through API results, that would not be useful for UI components. Furthermore, we may indeed have a prop named created that we may wish to combine with obj, therefore it is in our best interest to clear out any unneeded props to minimise compiler errors resulting from these conflicts.

Lets first define the props we’d like to remove in its own interface:

// interface for props we will dropinterface APIMetaProps {
_id: number;
created: number;
last_updated: number;
}

Now, what we need to do is define a generic type that will take a type, T, and exclude the _id, created and last_updated prop types from it. The resulting type will be called DropAPIMeta.

This is what the implementation looks like, introducing some advanced Typescript syntax that we will break down next:

// a generic type that drops unwanted metadata prop typestype DropAPIMeta<T> = Pick<T, Exclude<keyof T, keyof APIMetaProps>>

DropAPIMeta takes a type T and removes any of the properties that exist in APIMetaProps from it, thus resulting in a final type we can now use as the return type of our dropProps() function.

But wait — we have just introduced additional generics, Pick and Exclude type helpers, and two keyof operators, all in one line of code. Let’s examine what exactly is happening.

Working from the most embedded expression and moving up, let’s start with the Exclude type helper.

  • Exclude, as the name implies, allows us to remove certain types from another type. In the above example, we are removing keyof APIMetaProps from keyof T. Exclude takes two arguments — the type properties we wish to exclude properties from, and a union of the properties we wish to remove. The following example demonstrates a basic usage of Exclude, explicitly removing _id and created properties from a User type:
type User = {
_id: number;
created: number;
name: string;
};
type UserNoMeta = Exclude<User, '_id' | 'created'>
  • Pick does the opposite of Exclude — it picks (or takes) type properties that we explicitly define. The following will achieve the same type as our Exclude expression did above:
type UserNoMeta = Pick<User, 'name'>

Indeed, this is not very useful in scenarios where we do not know what types our props consist of, and therefore have no idea what to Pick— this is true in our case, as we have a generic type T to filter the unwanted props.

  • We can substitute our explicitly defined props with keyof <type>, which we have done in our final prop expression:
type DropAPIMeta<T> = Pick<T, Exclude<keyof T, keyof APIMetaProps>>;

The result of Exclude is then assigned as our second Pick argument, resulting in only the types of T we are intersted in.

Concretely, DropAPIMeta can take any type via the generic type T, and apply our Exclude and Pick type helpers, to pick the resulting types of the exclusion of APIMetaProps from type T.

Now we have a suitable return type in DropAPIMeta, let’s now implement our final utility function:

function dropAPIMeta<T extends DropAPIMeta>(obj: T): DropAPIMeta<T> {
let { _id, created, last_updated, ...rest } = obj;
return rest;
}

Note that T must extend DropAPIMeta, and therefore must include the _id, created and last_updated properties. If we wanted to loosen up on these rules we could include the optional ? declaration in the APIMetaProps interface to each of the properties.

The return type of DropAPIMeta<T> results in a return type that has our unwanted prop types removed. The destructure syntax of obj used in conjunction with the spread operator with …rest effectively separates our unwanted props, and rest is returned. The full solution is as follows:

// interface for unneeded propsinterface APIMetaProps {
_id: number;
created: number;
last_updated: number;
}

//generic type for props - unneeded props
type DropAPIMeta<T> = Pick<T, Exclude<keyof T, keyof APIMetaProps>>;
//utility function to remove unneeded props from an object
function dropAPI<T extends DropAPIMeta>(obj: T): DropAPIMeta<T> {
let { _id, created, last_updated, ...rest } = obj;
return rest;
}

Removing Props in HOC Functions

We can adopt the above solution for HOCs in the same way.

Note: To brush up on HOCs, more about them in my article dedicated to the subject.

The manipulation of removing unwanted props will happen inside the embedded HOC class within render(). Let’s name our HOC withoutAPIMeta and implement it:

function withoutAPIMeta<Props>(WrappedComponent: React.ComponentType<Props>) {
return class extends React.Component<Props> {

render() {
let { _id, created, last_updated, ...rest } = this.props;
return <WrappedComponent {...rest} />;
}
};
}

In the same way as our utility dropAPIMeta() utility function, we have destructured our props. The filtered rest props this time are spread in <WrappedComponent />!

To Conclude

This article has demonstrated how to structure and extend types within React Typescript projects, and has showcased some of the advanced Typescript helpers and operators to manage prop manipulation, which are particularly useful in higher-order components.

Although added complexity is introduced when adopting Typescript with React, this added complexity means fully-typed components throughout the project, ultimately living up to the purposes of Typescript: less bugs, scalability and reliability.

Ross Bulat

Written by

Author and programmer. Director @ JKRBInvestments.com

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade