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

huangapple go评论75阅读模式
英文:

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

问题

我有一个棘手的TypeScript问题。

假设我有这个带有size属性的Icon组件。size可以是"2"、"4"、"6"。我将这些值映射到预定义的Tailwind类。

我这样定义它:

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

一切都很好。但如果我想要根据屏幕大小不同来使用不同的尺寸怎么办?所以我想尝试像Tailwind那样的语法。

所以我将我的Icon组件重写如下:

type SizeValues = string;

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

这样可以正常工作,但我应该如何定义类型?我听说TypeScript将在未来支持正则表达式,这将使事情变得更容易,但现在是否有可能定义这个类型?

这不是一个真正的组件,所以请不要给我提供如何改进它的绝妙建议。我只是想知道如何定义我的size属性,以使它按照我编写的方式工作。

英文:

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 = &#39;2&#39; | &#39;4&#39; | &#39;6&#39;;

function Icon({size = &#39;4&#39;}: {size: SizeValues}) {
   const sizeMap = {
     &#39;2&#39;: &#39;w-2 h-2&#39;,
     &#39;4&#39;: &#39;w-4 h-4&#39;,
     &#39;6&#39;: &#39;w-6 h-6&#39;,
   };
 
   return &lt;span className={sizeMap[size]}&gt;My icon goes here&lt;/span&gt;
}

&lt;Icon size=&quot;sm&quot; /&gt;

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 = &#39;4&#39;}: {size: SizeValues}) {
   const sizeMap = {
     &#39;2&#39;: &#39;w-2 h-2&#39;,
     &#39;4&#39;: &#39;w-4 h-4&#39;,
     &#39;6&#39;: &#39;w-6 h-6&#39;,
     &#39;md:2&#39;: &#39;md:w-2 md:h-2&#39;,
     &#39;md:4&#39;: &#39;md:w-4 md:h-4&#39;,
     &#39;md:6&#39;: &#39;md:w-6 md:h-6&#39;,
     &#39;lg:2&#39;: &#39;lg:w-2 lg:h-2&#39;,
     &#39;lg:4&#39;: &#39;lg:w-4 lg:h-4&#39;,
     &#39;lg:6&#39;: &#39;lg:w-6 lg:h-6&#39;,
   };
 
   return &lt;span className={size.split(&#39; &#39;).map(s =&gt; sizeMap[s]).join(&#39; &#39;).trim()}&gt;My icon goes here&lt;/span&gt;
}

&lt;Icon size=&quot;2 md:4 lg:6&quot; /&gt;

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.

答案1

得分: 2

首先,我们需要将sizeMap提取到全局范围,并使用const assert声明它,以让编译器知道这是一个不可变的常量,并限制它不会扩展类型:

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;

接下来,我们需要为sizeMap的键创建一个类型:

type SizeMap = typeof sizeMap;
type SizeMapKeys = keyof SizeMap;

实现部分:

我们将创建一个接受字符串并返回它(如果有效)的类型;否则,返回never

伪代码:

让类型接受T - 要验证的字符串,Original - 原始字符串,AlreadyUsed - 已使用键的联合。

如果T是空字符串

  • 返回Original
    否则,如果TsizeMap的键(ClassName)开头,不包括AlreadyUsed,后跟一个空格和剩余字符串(Rest)。

  • 递归调用此类型,传递Rest作为要验证的字符串Original,并将ClassName添加到AlreadyUsed中。

否则,如果TsizeMap的键,不包括AlreadyUsed

  • 返回Original
    否则
  • 返回never

实现部分:

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;

我们必须为Item添加一个泛型参数,代表size

function Icon<T extends string | undefined>({
  size,
}: {
  size: _SizeValue<T>;
}) {
  return null;
}

由于组件中的size是可选的,我们将在SizeValue周围添加一个包装器,它将将string | undefined转换为string并将其传递给_SizeValue,此外,我们将为size添加一个默认值:

type SizeValue<T extends string | undefined> = _SizeValue<NonNullable<T>>;

function Icon<T extends string | undefined>({
  size = "2",
}: {
  size?: SizeValue<T> | "2";
}) {
  return null;
}

用法:

<Icon size="2" />;
<Icon size="md:2" />;
<Icon size="md:2 md:6" />;
<Icon size="md:2 md:6 lg:6" />;

// 预期错误
<Icon size="md:2 md:6 lg:5" />;

// 不允许重复
<Icon size="2 2" />;

[playground](https://www.typescriptlang.org/play?ts=5.0.4#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wG4AoctCAOwGd47gAvJAWRTDgF44BvcnAIAmfAC4CAdwC0wuAAtZ+ADSCCAFnFTp6hTpVr8ANi34ZRvSdVD8IACZjRE2w5lz7YxaOsEPm5x4yuh6Kmj4uYiYBrtIWIbEGNgA2AOaOpqlibnCZXokEmf4FaUE5aaH5+JlRxVmxZZ4JqgC+cCh0cNT0MBTkMACeYEhwAMos7Jw8cANDEJhwTKwcYBQzw2NLnADSSP0dvADWu3Oj48u9a3AA+htIAGooSQCuSAA8agAqcEgAHjBINDsHQYUGANBSPgA8qCUmDHt8-gCgQsYKDwVMPj4AIJJIgoOz9ACqdCQdgR-0BwNRYJSUxoSAAbkgoKoAHwY8lIjr4fBqAD8cGhwFhNEeagkX1+FORAAMACR8MGYZlwADCSXadAAcigQMMpVy4ABRH5oZ52N5qIRCW7LHZ7HzWuA4vEE4mkq1wVmtBVKlXIBicykotEpZoy-nXW4PZ5vAMwZSCmFwpKJl2oN0kskAHzVGro2t1SFZ4rgksRwZNZqeFtetu2uzoadxGaJWZLQgFQpFYqEEnpTKgF0G63GMZer3L0qpobguaegKQmDBpPZvBuY8eE61tC1TySGoARkk3h9WazepgFxhgLQ4ABJLqToPIkE0udwBcW5f0uysgAUfALOMUz4KIcDNBIQGLEgfISNGW6nuyuZgfgEEAJT8GoRAwE8UA0HAND7kkFDNJQryPneMHcAARMINFwAA9Be5AUV0wGsLRHj0UxLFsVR4xcQ47gOEYDHMRQ-EEdRNHcXAHgWNU4kseQjGMQiQwYKS3xQDgUCsZR0mCbJwnyaJDQAKzKb0amERAcB2E8YBJMAaAoP8HSPEkECSB6UkcUgtFyDxEn

英文:

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 = {
&#39;2&#39;: &#39;w-2 h-2&#39;,
&#39;4&#39;: &#39;w-4 h-4&#39;,
&#39;6&#39;: &#39;w-6 h-6&#39;,
&#39;md:2&#39;: &#39;md:w-2 md:h-2&#39;,
&#39;md:4&#39;: &#39;md:w-4 md:h-4&#39;,
&#39;md:6&#39;: &#39;md:w-6 md:h-6&#39;,
&#39;lg:2&#39;: &#39;lg:w-2 lg:h-2&#39;,
&#39;lg:4&#39;: &#39;lg:w-4 lg:h-4&#39;,
&#39;lg:6&#39;: &#39;lg:w-6 lg:h-6&#39;,
} 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&lt;
T extends string,
Original extends string = T,
AlreadyUsed extends string = never
&gt; = T extends &quot;&quot;
? Original
: T extends `${infer ClassName extends Exclude&lt;
SizeMapKeys,
AlreadyUsed
&gt;} ${infer Rest extends string}`
? _SizeValue&lt;Rest, Original, AlreadyUsed | ClassName&gt;
: T extends Exclude&lt;SizeMapKeys, AlreadyUsed&gt;
? Original
: never;

We have to add a generic parameter to Item that will represent the size.

function Icon&lt;T extends string | undefined&gt;({
size,
}: {
size: _SizeValue&lt;T&gt;;
}) {
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&lt;T extends string | undefined&gt; = _SizeValue&lt;NonNullable&lt;T&gt;&gt;;
function Icon&lt;T extends string | undefined&gt;({
size = &quot;2&quot;,
}: {
size?: SizeValue&lt;T&gt; | &quot;2&quot;;
}) {
return null;
}

Usage:

&lt;Icon size=&quot;2&quot; /&gt;;
&lt;Icon size=&quot;md:2&quot; /&gt;;
&lt;Icon size=&quot;md:2 md:6&quot; /&gt;;
&lt;Icon size=&quot;md:2 md:6 lg:6&quot; /&gt;;
// expected error
&lt;Icon size=&quot;md:2 md:6 lg:5&quot; /&gt;;
// no duplicates allowed
&lt;Icon size=&quot;2 2&quot; /&gt;;

playground

huangapple
  • 本文由 发表于 2023年6月2日 03:37:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/76385174.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定