Question: How can I define a type in TypeScript that's a string that only should contain words from a predefined list

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.


Answer:
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