In this repository I prototype an idea I had after seeing the Ant Icon
component, which looks like this:
<Icon type="clock-circle" />
It makes sense that this component would be designed like this, but I thought of a hack which might be funny so I decided to try and put it together.
At the core of the idea (like most of my ideas TBH) are TypeScript type unions. Uusually, these have a discriminator
field, e.g.: { type: 'unary'; operand: number; } | { type: 'binary'; leftOperand: number; rightOperand: number; }
.
This field is very useful, because then an instance of this type can be used with a switch
statement as demonstrated
below, and in each case
block, the instance is retyped to the concrete member type of the union with that
discriminator field value:
switch (operator.type) {
case 'unary': {
// `operator` is `{ type: 'unary'; operand: number; }` in this block
break;
}
case 'binary': {
// `operator` is `{ type: 'binary'; leftOperand: number; rightOperand: number; }` in this block
break;
}
case 'ternary': {
// This branch gives an error because `ternary` is not in `'unary' | 'binary'`
break;
}
default: {
// `operator` is `never` here
}
}
// `operator` is the union type here again
There is also another kind of type unions in TypeScript, ones that do not have the discriminator field, such as:
type Plus = { positive: true; negative: false; }
type Minus = { positive: false; negative: true; }
type Sign = Plus | Minus;
To tell which member type of the union an instance of the union type is, we need to use the instanceof
operator:
if (sign instanceof Plus) {
// `sign` is `Plus` here
}
// You get the idea
Note that the above example is actually broken, because Plus
and Minus
are just object literals, so they both have
type object
. They would have to both be classes in order for instanceof
to actually work, but for this example this
honestly doesn't matter, because:
There is a third way of telling what type something is, which is by implementing a function which returns a type
predicate. Type predicates look like this: arg is Type
. If a function returns this, it needs to return a boolean
value and TypeScript will be able to tell that arg
(which is any argument name from the function's argument list)
is of the type given by the type predicate.
function isPlus(sign: Sign): sign is Plus {
return (sign as Plus).positive === true && (sign as Plus).negative === false;
}
The scope where TypeScript can utilize this knowledge is given by the enclosing block which proces the type, so that will be a simple if most of the time:
if (isPlus(sign)) {
// `sign` is `Plus` here
} else {
// `sign` is `Minus` here because that is the only other type in the `Sign` type union after removing `Plus`
// If `Sign` had three meber types instead of two, `sign` would be the union of the remaining type here
}
// `sign` is `Sign` here
Putting all this together, we could have a React component whose props are a type union of distinct types, all of them
having a single field on it with the type of true
, so a boolean
literal which can only ever have the value of true
and then use these type predicate functions to tell which props we are dealing with in render
and render the
respective icon based on that.
Why force the field type to be true
? Because then you can use a TSX shorthand for boolean props, which looks like
this:
<Icon name />
// The above is the same as the below but nice
<Icon name={true} />
// Additionally this will produce an error:
<Icon name={false} />
With the type union props
we are not at risk of receiving multiple icon names in props, TypeScript coerces the props
to always be one of the finite number of possible props objects. With the true
type of the single field we also ensure
that no one is cheeky and attempts to pass in a false
for the icon name, which would be meaningless. And to prove that
this idea just keeps on giving, we could add any additional fields aside from the name so that the icon can have its
own props and we enforce they are only available for that icon.
A full example demonstrating this technique can be found in src/Icon.tsx
.
Notice in the example above that the individual member types can also individually extend other types for common props across multiple icons.
One drawback of this approach is dealing with default prop values. I am not sure how to solve this, nor have I tried. I do not use default props at all usually, so this has not been a problem for me.
Another point to be made is that one could of course just as well explore this single Icon
component to an individual
component for each icon with its specific props etc. I usually do this, I do not use this hack in production myself, but
I like the idea.
This switch
may well be slower, but it will be much neater and the speed does
not really matter for such a small library.
switch (true) {
case this.isX(this.props) {
return <X />;
}
case this.isY(this.props) {
return <Y />;
}
case this.isZ(this.props) {
return <Z />;
}
}
throw new Error(`Invalid props!`);