TypeScript - 是否可以基于模式匹配或长度验证字符串类型?

7
考虑以下组件,它使用名为 "styled-components" 的库来创建预定义样式的 "Text" 组件:
const StyledText = styled(Text)`
  font-family: Roboto;
  color: ${(props: ITextProps) => props.color || '#000' }; 
`

其中ITextProps是:

interface ITextProps {
  color: string;
}

有没有办法强制只传递有效的十六进制字符串到我的组件中?

理想情况下,颜色属性应该始终匹配模式/#\d{3}(\d{3})?/g,即#后跟至少3个数字,可选地再跟3个数字。如果不可能,那么是否有一种方法可以强制要求字符串长度为4或7个字符?

我的研究无果,所以我想知道是否有人知道如何在TypeScript中实现这种行为。


1
请翻译以下与编程有关的内容,从英文到中文。仅返回翻译后的文本:https://github.com/Microsoft/TypeScript/issues/6579#issuecomment-261519733,从那时起就会有更多关于正则表达式作为类型的讨论。 - Amir-Mousavi
这里有一个实现示例:https://dev59.com/RcHqa4cB1Zd3GeqPtSWv#68068969 - gaitat
1个回答

12

2021年6月13日:已更新至TS4.1+


不完全是在编译时,没有。有一个建议(现在在microsoft/TypeScript#41160)允许正则表达式验证的字符串类型,但不清楚它是否会被实现。如果您想去那个建议并提供一个令人信服的用例,这可能有所帮助(但也可能没有真正的帮助)。
您可以尝试使用模板文字类型来以编程方式生成匹配每个可接受的字符串字面量的大联合。如果您只需要三个左右的数字,这甚至有点起作用。
type UCaseHexDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | 
  '8' | '9' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F'
type HexDigit = UCaseHexDigit | Lowercase<UCaseHexDigit>
type ValidThreeDigitColorString = `#${HexDigit}${HexDigit}${HexDigit}`;
// type ValidThreeDigitColorString = "#000" | "#001" | "#002" | "#003" | "#004" | "#005" 
// | "#006" |  "#007" | "#008" | "#009" | "#00A" | "#00B" | "#00C" | "#00D" | "#00E" 
// | "#00F" | "#00a" | "#00b" | "#00c" | "#00d" |  // "#00e" 
// | ... 10626 more ... | "#fff"

但是由于这种模板文字类型只能处理成员数量在数万个左右的联合类型,如果您尝试使用六位数字,它将会出现错误:

type ValidSixDigitColorString =
    `#${HexDigit}${HexDigit}${HexDigit}${HexDigit}${HexDigit}${HexDigit}`; // error!
//  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Expression produces a union type that is too complex to represent

所以你需要使用一个变通方法。


一种解决方法是使用模板字面量类型作为通用约束,而不是将ValidColorString作为具体类型。相反,有一个类型AsValidColorString<T>,它接受一个字符串类型的T并对其进行检查以查看它是否有效。如果是,则保持不变。如果不是,则返回一个与错误值“接近”的有效颜色字符串。例如:
type ToHexDigit<T extends string> = T extends HexDigit ? T : 0;
type AsValidColorString<T extends string> =
    T extends `#${infer D1}${infer D2}${infer D3}${infer D4}${infer D5}${infer D6}` ?
    `#${ToHexDigit<D1>}${ToHexDigit<D2>}${ToHexDigit<D3>}${ToHexDigit<D4>}${ToHexDigit<D5>}${ToHexDigit<D6>}` :
    T extends `#${infer D1}${infer D2}${infer D3}` ?
    `#${ToHexDigit<D1>}${ToHexDigit<D2>}${ToHexDigit<D3>}` :
    '#000'

const asTextProps = <T extends string>(
   textProps: { color: T extends AsValidColorString<T> ? T : AsValidColorString<T> }
) => textProps;

这很复杂,主要是将字符串T拆分并检查每个字符,将不良字符转换为0。然后,您不会将某些东西注释为TextProps,而是调用asTextProps进行验证:

const textProps = asTextProps({
    color: "#abc" // okay
})

const badTextProps = asTextProps({
    color: "#00PS1E" // error
//  ~~~~~
//  Type '"#00PS1E"' is not assignable to type '"#00001E"'.(2322)
})

这在编译时可以工作,但可能会带来更多的麻烦。

最后,您可以退回到TS4.1之前的解决方案,并使用用户定义类型保护创建一个类似名义类型的string子类型,以缩小string值范围...然后跳过各种障碍来使用它:

    type ValidColorString = string & { __validColorString: true };

    function isValidColorString(x: string): x is ValidColorString {
      const re = /#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?/g; // you want hex, right?
      return re.test(x);
    }

使用方法:

    const textProps: ITextProps = {
      color: "#abc"
    }; // error, compiler doesn't know that "#abc" is a ValidColorString
    
    const color = "#abc";
    if (isValidColorString(color)) {
      const textProps2: ITextProps = {
        color: color
      }; // okay now
    } else {
      throw new Error("The world has ended");
    }

后者并不完美,但至少让您更接近实施这些约束。

希望这能给您一些想法,祝你好运!

代码的Playground链接


1
嗯,这有点遗憾,但我想总比没有好。最终我认为这不适合我的使用情境,但我仍然感谢提供的信息和例子。 - Robbie Milejczak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接