search

Home  >  Q&A  >  body text

How to define a type in TypeScript that is a string that can only contain words from a predefined list

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粉139351297P粉139351297319 days ago490

reply all(1)I'll reply

  • P粉509383150

    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

    • Returnoriginal otherwise
    • ReturnNever

    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" />;

    Playground

    reply
    0
  • Cancelreply