One really great feature of TypeScript is the union type feature. When defining an interface, I am able to define a property of an interface as a {String} or I can optionally make use of literally types {"option1" | "option2" | "option3"} which would give better type checking in a field that requires a very specific value. This is often used for component props like this:
interface ButtonProps {
ofType?: 'button' | 'span' | 'a';
// ...otherTypes
}
This is fantastic and gives me really good intellisense when I want to use these props
export function Page() {
return (
<div>
{/* ✅ - All good */}
<Button ofType="button">Click me</Button>
{/* ❌ - Gives type error */}
<Button ofType="h1">Click me</Button>
</div>
);
}
However, this has a shortcoming. Let's say that we want properties only for a certain type of element. In the case of the button, let's say that we want to have an href and target prop when the button is set to ofType: 'a'. The typical approach is to have optional properties that are only applied in the component under the right conditions.
interface ButtonProps {
ofType?: 'button' | 'span' | 'a';
target?: string; // We ignore this property if the button is not an <a /> tag
href?: string; // We ignore this property if the button is not an <a /> tag
// ...otherTypes
}
We would now be able to do this
export function Page() {
return (
<div>
{/* ❌- Does nothing */}
<Button ofType="button" href="/" target="_self">
Click me
</Button>
{/* ✅ - Gives us a valid <a /> */}
<Button ofType="a" href="/" target="_self">
Click me
</Button>
</div>
);
}
Now, the problem with this is that I could create the <Button ofType="a" /> and omit the href property and I will get no type errors. Also, I will not get any type errors if I add that property to a variant that shouldn't have it. This is where union types come in. We could extend the ButtonProps interface like this.
interface BaseButtonProps {
ofType?: 'button' | 'span' | 'a';
// ...otherTypes
}
interface ButtonButtonProps extends BaseButtonProps {
ofType: 'button';
// ...other <button /> specific types
}
interface SpanButtonProps extends BaseButtonProps {
ofType: 'button';
// ...other <button /> specific types
}
interface AnchorButtonProps extends BaseButtonProps {
ofType: 'a';
href: string; // required
target?: string; // not required
// ... other <a /> specific types
}
type ButtonProps = ButtonButtonProps | SpanButtonProps | AnchorButtonProps;
This now tells type script that it should look at the ofType property and determine from that which of the interfaces we have defined it should check the button props against. Now, we can do this:
export function Page() {
return (
<div>
{/* ❌- Type error "href" is not a property of ButtonProps. */}
<Button ofType="button" href="/" target="_self">
Click me
</Button>
{/* /✅ - Gives us a valid <a /> */}
<Button ofType="a" href="/" target="_self">
Click me
</Button>
{/* ❌- Type error missing property "href" */}
<Button ofType="a">Click me</Button>
</div>
);
}