Learn more about our current job openings and benefits of working at FSL.
Detailed reviews and feedback from past and current clients.
Get to know the Management Team behind FullStack Labs.
Our step-by-step process for designing and developing new applications.
Writings from our team on technology, design, and business.
Get answers to the questions most frequently asked by new clients.
Learn about our company culture and defining principles.
A high level overview of FullStack Labs, who we are, and what we do.
A JavaScript framework that allows rapid development of native Android and IOS apps.
A JavaScript framework maintained by Facebook that's ideal for building complex, modern user interfaces within single page web apps.
A server side programming language known for its ease of use and speed of development.
A lightweight and efficient backend javascript framework for web apps.
An interpreted high-level programming language great for general purpose programming.
A JavaScript framework maintained by Google that addresses many of the challenges encountered when building single-page apps.
A JavaScript framework that allows developers to build large, complex, scalable single-page web applications.
A progressive JavaScript framework known for its approachability, versatility, and performance.
A progressive JavaScript framework known for its approachability, versatility, and performance.
A progressive JavaScript framework known for its approachability, versatility, and performance.
A progressive JavaScript framework known for its approachability, versatility, and performance.
A progressive JavaScript framework known for its approachability, versatility, and performance.
A progressive JavaScript framework known for its approachability, versatility, and performance.
A progressive JavaScript framework known for its approachability, versatility, and performance.
View a sampling of our work implemented using a variety of our favorite technologies.
View examples of the process we use to build custom software solutions for our clients.
View projects implemented using this javascript framework ideal for building complex, modern user interfaces within single page web apps.
View projects implemented using this framework that allows rapid development of native Android and IOS apps.
View projects implemented using this backend javascript framework for web apps.
View projects implemented using this high-level programming language great for general purpose programming.
View projects implemented using this server side programming language known for its ease of use and speed of development.
We have vast experience crafting healthcare software development solutions, including UI/UX Design, Application Development, Legacy Healthcare Systems, and Team Augmentation. Our development services help the healthcare industry by enhancing accessibility, productivity, portability, and scalability.
We offer a range of custom software development solutions for education companies of all sizes. We're experts in Education Software Development and specialists in enhancing the learning experience across web, mobile, and conversational UI.
We're experts in developing Custom Software Solutions for the Logistics Industry. Our work offered a whole new and more efficient way for Logistics companies to manage their crucial operations.
We partner with various construction industry organizations to build custom software development solutions. Our Construction Software Development Services allow construction companies to manage projects, resources, and documentation.
We have vast experience crafting healthcare software development solutions, including UI/UX Design, Application Development, Legacy Healthcare Systems, and Team Augmentation. Our development services help the healthcare industry by enhancing accessibility, productivity, portability, and scalability.
We offer a range of custom software development solutions for education companies of all sizes. We're experts in Education Software Development and specialists in enhancing the learning experience across web, mobile, and conversational UI.
We're experts in developing Custom Software Solutions for the Logistics Industry. Our work offered a whole new and more efficient way for Logistics companies to manage their crucial operations.
We partner with various construction industry organizations to build custom software development solutions. Our Construction Software Development Services allow construction companies to manage projects, resources, and documentation.
Learn more about our current job openings and benefits of working at FSL.
Detailed reviews and feedback from past and current clients.
Get to know the Management Team behind FullStack Labs.
Our step-by-step process for designing and developing new applications.
Writings from our team on technology, design, and business.
Get answers to the questions most frequently asked by new clients.
Learn about our company culture and defining principles.
A high level overview of FullStack Labs, who we are, and what we do.
A JavaScript framework that allows rapid development of native Android and IOS apps.
A JavaScript framework maintained by Facebook that's ideal for building complex, modern user interfaces within single page web apps.
A server side programming language known for its ease of use and speed of development.
A lightweight and efficient backend javascript framework for web apps.
An interpreted high-level programming language great for general purpose programming.
A JavaScript framework maintained by Google that addresses many of the challenges encountered when building single-page apps.
A JavaScript framework that allows developers to build large, complex, scalable single-page web applications.
A progressive JavaScript framework known for its approachability, versatility, and performance.
A dynamic programming language used in all sorts of web and mobile applications.
A cross-platform programming language designed to run robust applications on any device.
A UI toolkit used to build natively compiled applications from a single codebase.
A functional programming language that’s ideal for scalability, maintainability, and reliability.
A Customer Relationship Management (CRM) platform that seamlessly integrates with your business operations.
A high-performance programming language that makes it easy to build simple, reliable, and efficient software.
View a sampling of our work implemented using a variety of our favorite technologies.
View examples of the process we use to build custom software solutions for our clients.
View projects implemented using this javascript framework ideal for building complex, modern user interfaces within single page web apps.
View projects implemented using this framework that allows rapid development of native Android and IOS apps.
View projects implemented using this backend javascript framework for web apps.
View projects implemented using this high-level programming language great for general purpose programming.
View projects implemented using this server side programming language known for its ease of use and speed of development.
This article explains how to create correct, intuitive APIs for React components in TypeScript by using prop values to overload React component interfaces.
A quick disclaimer: This article assumes intermediate to advanced knowledge of both TypeScript and React. While it will cover some basic refreshers, it would be best for readers to have a moderate level of understanding in using TypeScript with React first.
To start with some basics, it’s good to have a refresher as to what TypeScript is and why it’s useful. TypeScript is a programming language that is a superset of the JavaScript programming language, and its use has exploded over the last few years. TypeScript allows us to add static type-checking to our JavaScript code which can have a huge number of benefits, including easier maintenance and debugging and simpler code. For developers migrating to JavaScript from strongly typed languages, TypeScript is usually a sight for sore eyes.
To learn more about TypeScript and how it can benefit an organization, check out Jose Arce’s TypeScript for Enterprise Apps. For actually getting started with TypeScript, there is no better starting point than TypeScript’s website and docs.
This article won’t list all of the potential benefits of using TypeScript but will talk largely about one of the most obvious and impactful one — component interfaces. Component interfaces allow us to create our own clear and documented API for each component written. This makes code more maintainable for future developers, prevents bugs by checking properties and data-types passed between components, and makes implementation intentions far more obvious. Check out Brian Smith’s article, Make useContext Data More Discoverable with TypeScript, for more on this topic.
Here is a simple example showing the difference between a component implemented with and without TypeScript.
-- CODE language-jsx keep-markup --
/* ItemList.js */
const Item = (props) => {
return (
<li>{`${props.name}: ${props.quantity}`}</li>
)
}
/* ItemList.js */
const ItemList = (props) => {
return (
<ul>
{props.items.map((item) => <Item key={item.name} {...item} />)}
</ul>
)
}
While the components themselves may be simple enough to understand, there are some problems. For one, a future developer will need to dig into the implementation of each component to understand what props can be passed to ItemList. That’s not very good documentation, is already more work for the future developers, and is harder to maintain.
As the components gain complexity, this problem compounds over time and becomes more and more difficult to reason about. People make mistakes and TypeScript is really just a way of making it harder for people to make mistakes.
Here is a new example implementing the same solution with TypeScript:
-- CODE language-jsx keep-markup --
/* Item.tsx */
export interface ItemProps {
name: string;
quantity: string;
}
const Item: React.FC<ItemProps> = (props) => {
return (
<li>{`${props.name}: ${props.quantity}`}</li>
)
}
/* ItemList.tsx */
export interface ItemListProps {
items: Array<ItemProps>
}
const ItemList: React.FC<ItemListProps> = (props) => {
return (
<ul>
{props.items.map((item) => <Item key={item.name} {...item}/>)}
</ul>
)
}
Now there is a clearly documented interface informing developers about component usage while also protecting the components from being passed incorrect properties and data (though not at run-time).
For a simple example of this in action, look at what happens when passing a label property to Item without specifying it in the interface.
A detailed error is provided showing the developer that this isn’t what was expected. This added layer of safety allows for the safer spreading of props over components as shown by {...item} in the above example. However, because TypeScript only checks types at compile time, spreading should still be avoided in cases where data in the props object is potentially unpredictable, or where the typed component is being consumed by a JavaScript application rather than a TypeScript one. Without TypeScript or propTypes, or in the listed exceptions, prop spreading is usually considered unsafe because of the possibility of unknown attributes being added to the DOM.
Typically, it’s considered best practice to keep component interfaces simple, small, and reflective of a singular purpose. Developers following that thought process will go far with TypeScript and React. However, like anything, there are exceptions to this rule depending on the use case and the context of implementation.
Exceptions to the simplicity rule often increase as components need to become more general or abstract. Common examples of the need to create increasingly complex components often exist in re-usable component libraries or in components that inherit some kind of styling or theming irrelevant to the actual element used. In these cases, creating correct types can become increasingly difficult and much less than straightforward. This complexity often leads to typings being implemented incorrectly or to developers typing their components as any. In both cases, the benefits of TypeScript are largely canceled out. In the case of any, the developer now has no information on the intention of the data passed to the component. In the case of an incorrect interface, developers are now guided towards incorrect implementations. Incorrect typings and unnecessary use of any are cases where TypeScript actually adds maintenance and complexity to a codebase rather than reducing it.
Here’s a simple example of some requirements of an interface that quickly becomes difficult to implement correctly. Say an app needs to create a simple component that is going to have some themeable styling logic, but the actual element could be anything. This should:
This is less straightforward than it might seem. The correct typing now depends on a) selecting the correct interface among all HTML elements based on the prop values passed in, and b) filtering React component props based on a subset of these elements.
Let’s look at some solutions that seem valid but aren’t.
-- CODE language-jsx keep-markup --
const Element: React.FC<any> = (
{ as: Component = "div", ...props },
) => {
return <Component {...props} />;
};
This meets requirement 1, but not 2 and 3.
What about an interface that extends a union of element interfaces? We will run into a limitation there as well:
We could try typing the parameter directly with a union type:
-- CODE language-jsx keep-markup --
type BaseAttributes = React.ComponentPropsWithRef<'div'> | React.ComponentPropsWithRef<'a'>
const Element = ({ as: Component = "div", ...props }: {
as?: React.ElementType
} & BaseAttributes) => {
return <Component { ...props } />;
}
However, we will soon see this still doesn’t pass our requirements 2 or 3, and our component interface surfacing as little better than any.
-- CODE language-jsx keep-markup --
export default function App() {
return (
<div className="App">
<Element as="div" href="https://www.google.com">{'Hello world!'}</Element>
</div>
);
}
In order to solve this, we will need to lean on a few relatively abstract TypeScript concepts and to create a few special types.
First, let’s create our prop interface for our Element component. We won’t want an empty interface typically, but we’re assuming in this step that we’ll be adding properties here later.
-- CODE language-jsx keep-markup --
export interface ElementProps {}
Next, we will create our simple types. Let’s create a type we’ll use to meet requirement number 3. We’ll make a union of element keys representing our void elements (elements not accepting children).
-- CODE language-jsx keep-markup --
export type VoidElement =
| 'area'
| 'base'
| 'br'
| 'col'
| 'hr'
| 'img'
| 'input'
| 'link'
| 'meta'
| 'param'
| 'command'
| 'keygen'
| 'source'
Now that we’ve created the VoidElement type, let’s create a conditional type omitting the children property in the event a void element is passed. TypeScript allows us to use ternary-like operators to create conditional types.
-- CODE language-jsx keep-markup --
export type OmitChildrenFromVoid<C extends React.ElementType> =
C extends VoidElement ?
Omit<React.ComponentPropsWithRef<C>, 'children'>
: React.ComponentPropsWithRef<C>
Now if a key belonging to the VoidElement type is passed as a parameter to our new type it will return the element interface without the children prop.
Finally, we’ll move on to our biggest problem. How do we infer our component interface from the value of our as property?
To do this we can first define a function type interface. TypeScript allows us to create interfaces describing the shape and overloads of a function. The React.FC typing is done similar to what we will see here. We will type our React component with this interface once completed.
-- CODE language-jsx keep-markup --
export interface OverloadedElement {
<C extends React.ElementType>(
props: { as: C }
): JSX.Element;
}
This is a good start but we aren’t quite done. We still need to do two major things. First, we want to handle a default overload for when our as prop doesn’t exist and we want to be able to pass in our own prop interfaces to intersect element attributes.
-- CODE language-jsx keep-markup --
export interface OverloadedElement<P> {
<C extends React.ElementType>(
props: { as: C } & P>
): JSX.Element;
(props: P): JSX.Element;
}
Finally, we want to intersect our custom props with the possible attributes for our selected element and we want to use our OmitChildrenFromVoid type from earlier to meet requirement 3. For correctness, we also want to omit properties from the React.ComponentPropsWithRef<C> type that might also be implemented in our custom prop interface, ElementProps.
-- CODE language-jsx keep-markup --
export interface OverloadedElement<P> {
<C extends React.ElementType>(
props: { as: C } & P & Omit<OmitChildrenFromVoid<C>, keyof P>
): JSX.Element;
(props: P & Omit<React.ComponentPropsWithRef<'div'>, keyof P>): JSX.Element;
}
const Element: OverloadedElement<ElementProps> = (
{ as: Component = "div", ...props },
) => {
return <Component {...props} />;
};
At this point, things should be working the way we want. Though things still aren’t perfect. Why?
The issue here is with our use of Omit. Omit does not behave in the way most would expect over a union type. Omit does not get applied distributively over the subtypes of a union, so it’s potentially unsafe to use Omit if we don’t know whether the types contain a union. In order to do this, we need to create a new type that will apply Omit distributively. Luckily, TypeScript allows us to use conditional types to apply types distributively over a union type.
Let’s take a look at the final code:
-- CODE language-jsx keep-markup --
export interface ElementProps {}
export type DistributiveOmit<T, K extends keyof any> =
T extends any ? Omit<T, K> : never;
export type VoidElement =
| 'area'
| 'base'
| 'br'
| 'col'
| 'hr'
| 'img'
| 'input'
| 'link'
| 'meta'
| 'param'
| 'command'
| 'keygen'
| 'source'
export type OmitChildrenFromVoid<C extends React.ElementType> =
C extends VoidElement ?
Omit<React.ComponentPropsWithRef<C>, 'children'> :
React.ComponentPropsWithRef<C>
export interface OverloadedElement<P> {
<C extends React.ElementType>(
props: { as: C } & P & DistributiveOmit<OmitChildrenFromVoid<C>, keyof P>
): JSX.Element;
(props: P & DistributiveOmit<React.ComponentPropsWithRef<'div'>, keyof P>): JSX.Element;
}
const Element: OverloadedElement<ElementProps> = (
{ as: Component = "div", ...props },
) => {
return <Component {...props} />;
};
Bonus tip: A simple way to extend this to include unionized interface overloading to your custom props could be to follow a pattern similar to this when creating your prop interfaces.
-- CODE language-jsx keep-markup --
export interface ElementAlt1Props { b?: 'string'; c?: string }
export interface ElementAlt2Props { b?: 'number'; c?: number; }
export type ElementProps = ElementAlt1Props | ElementAlt2Props;
Here we see mismatching the element type with an invalid attribute will throw an error.
Correcting the element type corrects the error.
Void elements properly complain when children are present.
Void elements stay happy without children.
Success! As you can see, TypeScript is a great tool and can add a lot of benefits to a React project. It’s used in lots of projects including projects here at FullStack Labs. This article was meant to show how much power TypeScript can give your components, but also how the correct implementations can really prevent easy-to-miss bugs and supercharge your development.
We’d love to learn more about your project.
Engagements start at $75,000.