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.
P粉5093831502024-01-11 10:57:21
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:
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', } as const;
Next, we need to get the type of key of sizeMap
:
type SizeMap = typeof sizeMap; type SizeMapKeys = keyof 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 string
Returnoriginal
Otherwise, if T
begins with the key of the size map (ClassName
), excluding AlreadyUsed
, followed by a space and the remaining string (break
).
Call this type recursively, passing Rest
as a string to validate Original
, and AlreadyUsed
with ClassName< /code> added to it.
Else if T
is the key of the size map, excluding AlreadyUsed
original
otherwiseNever
accomplish:
type _SizeValue< T extends string, Original extends string = T, AlreadyUsed extends string = never > = T extends "" ? Original : T extends `${infer ClassName extends Exclude< SizeMapKeys, AlreadyUsed >} ${infer Rest extends string}` ? _SizeValue<Rest, Original, AlreadyUsed | ClassName> : T extends Exclude<SizeMapKeys, AlreadyUsed> ? Original : never;
We must add a common parameter to the Item
to represent the size
.
function Icon<T extends string | undefined>({ size, }: { size: _SizeValue<T>; }) { return null; }
Since size
is optional in the component, we will add a wrapper around SizeValue
which will convert string | undefined
to string
and pass it to _SizeValue
, in addition we will add a default value for the size:
type SizeValue<T extends string | undefined> = _SizeValue<NonNullable<T>>; function Icon<T extends string | undefined>({ size = "2", }: { size?: SizeValue<T> | "2"; }) { return null; }
usage:
<Icon size="2" />; <Icon size="md:2" />; <Icon size="md:2 md:6" />; <Icon size="md:2 md:6 lg:6" />; // expected error <Icon size="md:2 md:6 lg:5" />; // no duplicates allowed <Icon size="2 2" />;