I have a tricky TypeScript question.
Let say I have this Icon component with the prop size. Size can be "2", "4", "6". I map these values to predefined tailwind classes.
So I type it like
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" />

Everything is fine. But what if I wanna have different sizes depending on what screen size I have. So I wanna try to have like tailwinds nice syntax.
So I rewrite my Icon component to 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" />

That works fine, but how do I type it? I read TypeScript will support regex in the future. That will make it easier, but is it possible to type this now?
This is not a real component so please don't give me awesome suggestions how I can improve it. I just wanna know how I can type my size prop so it works the way I've coded it.

How can I define a type in TypeScript that's a string that only should contain words from a predefined list
First, we need to extract sizeMap into the global scope, and const assert it to let the compiler know that this is immutable constant and restrict  it from widening types:
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 a type for the keys of the sizeMap:
type SizeMap = typeof sizeMap;
type SizeMapKeys = keyof SizeMap;

Implementation:
We will create a type that accepts a string and returns it if it is valid; otherwise, return never.
Pseudo-code:
Let type accept T - string to validate, Original - original string, AlreadyUsed - union of already used keys.
If T is an empty string

return Original
Else if T starts with keys of the size map (ClassName), excluding AlreadyUsed, followed by a space and the remaining string(Rest).

Recursively call this type, passing Rest as a string to validate Original, and the AlreadyUsed with ClassName added to it.


Else if T is the key of the size map excluding AlreadyUsed

return Original
else
return never

Realization:
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 have to add a generic parameter to Item that will 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 the SizeValue which will turn string | undefined to string and pass it to _SizeValue, additionally we will add a default value for 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" />;

playground
