英文:
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 = '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.
答案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
否则,如果T
以sizeMap
的键(ClassName
)开头,不包括AlreadyUsed
,后跟一个空格和剩余字符串(Rest
)。 -
递归调用此类型,传递
Rest
作为要验证的字符串Original
,并将ClassName
添加到AlreadyUsed
中。
否则,如果T
是sizeMap
的键,不包括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" />;
英文:
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 ifT
starts with keys of the size map (ClassName
), excludingAlreadyUsed
, followed by a space and the remaining string(Rest
). -
Recursively call this type, passing
Rest
as a string to validateOriginal
, and theAlreadyUsed
withClassName
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" />;
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论