I have a tricky TypeScript problem.
Suppose I have an icon component with prop size. Size can be "2", "4", "6". I map these values to a predefined tailwind class.
So I typed like this
type SizeValues = '2' | '4' | '6'; function Icon({size = '4'}: {size: SizeValues}) { const sizeMap = { '2': 'w-2 h-2', '4': 'w-4 h-4', '6': 'w-6 h-6', }; return <span className={sizeMap[size]}>My icon goes here</span> } <Icon size="sm" />
all is well. But what if I want to have different sizes based on my screen size? So I want to try to have good grammar that goes smoothly.
So I rewrote the Icon component to the following:
type SizeValues = ??? function Icon({size = '4'}: {size: SizeValues}) { const sizeMap = { '2': 'w-2 h-2', '4': 'w-4 h-4', '6': 'w-6 h-6', 'md:2': 'md:w-2 md:h-2', 'md:4': 'md:w-4 md:h-4', 'md:6': 'md:w-6 md:h-6', 'lg:2': 'lg:w-2 lg:h-2', 'lg:4': 'lg:w-4 lg:h-4', 'lg:6': 'lg:w-6 lg:h-6', }; return <span className={size.split(' ').map(s => sizeMap[s]).join(' ').trim()}>My icon goes here</span> } <Icon size="2 md:4 lg:6" />
This works great, but how do I enter it? I read that TypeScript will support regular expressions in the future. This will make things easier, but can I type this now?
This is not a real component, so please don't give me good suggestions on how to improve it. I'm just wondering how to input my size attribute so that it works the way I coded it.
First, we need to extract the
sizeMap
into the global scope and const assert to let the compiler know that this is an immutable constant and restrict it from expanding the type:Next, we need to get the type of key of
sizeMap
:Implementation: We'll create a type that accepts a string and returns it if the string is valid; otherwise,
never
is returned.pseudocode:
Let the type accept
T
- the string to be validated,Original
- the original string,AlreadyUsed
- the union of used keys.If
T
is an empty stringReturn
original
Otherwise, ifT
begins with the key of the size map (ClassName
), excludingAlreadyUsed
, followed by a space and the remaining string (break
).Call this type recursively, passing
Rest
as a string to validateOriginal
, andAlreadyUsed
withClassName added to it.
Else if
T
is the key of the size map, excludingAlreadyUsed
original
otherwiseNever
accomplish:
We must add a common parameter to the
Item
to represent thesize
.Since
size
is optional in the component, we will add a wrapper aroundSizeValue
which will convertstring | undefined
tostring
and pass it to_SizeValue
, in addition we will add a default value for the size:usage:
Playground