如何将受约束的字符串传递给使用相同约束的参数作为参数的类型

How to pass a constrained string to a type using as parameters a parameter using that same constraint

我可以建立这样的数字:

type Digits = '0'| '1'| '2'| '3'| '4'| '5'| '6'| '7'| '8'| '9'
type NonZero = Exclude <Digits, '0'>
type Negative = '-'

type PositiveNumbers =
  | NonZero
  | `${NonZero}${Digits}`
  | `${NonZero}${Digits}${Digits}`
  | `${NonZero}${Digits}${Digits}${Digits}`
  | `${NonZero}${Digits}${Digits}${Digits}${Digits}`
  | `${NonZero}${Digits}${Digits}${Digits}${Digits}${Digits}`  // Error: Expression produces a union type that is too complex to represent.(2590)

type Numbers =
  | PositiveNumbers
  | `${Negative}${PositiveNumbers}`

但是我只能使用 -9999999999 中的数字,除此之外,在其他类型中使用类型 Numbers 会使编译器陷入困境。所以我使用一种类型来表示数字作为约束,如下所示:

type ConstrainNumber <N extends string> =
  N extends '0'
  ? unknown
  : N extends `${infer Char}${infer Rest}`
    ? Char extends '0'
      ? never
      : Char extends Negative
        ? ConstrainNumber <Rest>
        : ConstrainNumberRec <N, Exclude <Digits, '0'>>
    : never

type ConstrainNumberRec <S extends string, D = Digits> =
  S extends `${infer Char}${infer Rest}`
  ? Char extends D ? ConstrainNumberRec <Rest> : never
  : unknown

在类型上使用它时,它按预期工作:

type UseNumbers <N extends string & _N, _N = ConstrainNumber <N>> = N

type testValid = UseNumbers <'2875467'>
type testInvalid = UseNumbers <'02875467'> // Expected error: Type 'string' does not satisfy the constraint 'never'.(2344)

但是当我使用使用 UseNumbers 的类型时它会中断

type UseUseNumbers <N extends string & _N, _N = ConstrainNumber <N>> = 
  UseNumbers <N> // Unwanted Error: type 'N' does not satisfy the constraint 'string & ConstrainNumber<N>'

我如何将约束字符串传递给使用相同约束的参数作为参数的类型?
而且,我不确定猜它为什么会坏?在 UseNumbers <N> N 上是否已经有约束?

playground

主要问题是 generic parameter defaults are not generic constraints:

type Foo<T extends number> = T;
type BadFoo = Foo<string>; // error, not satisfying constraint

type Bar<T = number> = T;
type DefaultBar = Bar; // number
type StillGoodBar = Bar<string>; // string

上面Foo中的类型参数Tconstrainednumber,而在Bar中是不受约束,但只是 默认 number。所以不能写Foo<string>。如果你只写 Bar 它将是 Bar<number>,但是没有什么能阻止某人写 Bar<string>。这不是错误,编译器无法在 Bar 内部假设 T 将是 number.

的子类型

在这个定义中,

type UseUseNumbers<N extends string & _N, _N = ConstrainNumber<N>> = ...

_N 类型参数不受约束。它可以是任何东西,即使它 默认 ConstrainNumber<N>。只有 N 被限制为 string & _N.

这里的主要用例是没有人指定 _N,并且 UseUseNumbers<N> 等效于 UseUseNumbers<N, ConstrainNumber<N>>,当且仅当 N extends string & ConstrainNumber<N> 时才编译。这是避免如果您编写 type Oopsie<N extends string & ConstrainNumber<N>> 会发生的循环约束错误的巧妙方法,但该技巧只有在 _N 不依赖于 N.[= 的可能性保持开放的情况下才有效。 62=]

因此编译器无法知道 _N 是什么特别的东西。一些流氓开发人员可能会编写 UseUseNumbers<string, unknown>UseUseNumbers<"abc","abc" | "def"> 或其他一些疯狂的 off-label 使用 UseUseNumbers 碰巧编译。这可能是不可避免的(尽管问题的可避免性或缺乏是 off-topic)但也不太可能实际发生,特别是如果您记录 _N 仅供“内部使用”(但同样,off-topic).您要考虑多少这种可能性取决于您。


无论如何,编译器对

感到不满
type UseUseNumbers<N extends string & _N, _N = ConstrainNumber<N>> =
  UseNumbers<N> // <-- error

为什么?因为 UseNumbers<N> 的计算结果为 UseNumbers<N, ConstrainNumber<N>>,它(类似于 UseUseNumbers)仅在 N extends string & ConstrainNumber<N> 时才编译。但是编译器并不知道当前的N,它只知道扩展string & _N,而_N是不受约束的。所以它不编译。


解决这个问题的方法是明确假设没有人会在 UseUseNumbers 中弄乱 _N,并将其传递给 UseNumbers:

type UseUseNumbers<N extends string & _N, _N = ConstrainNumber<N>> =
  UseNumbers<N, _N> // <-- this fixes it

由于UseNumbers中的_N是无约束的,所以可以写成UseNumbers<N, _N>,当且仅当N extends string & _N时编译通过。但是我们知道 UseUseNumbersN 的定义是正确的。只要人们写 UseUseNumbers<N> 并且不传递第二个类型的参数,那么这将根据需要评估为 UseNumbers<N>

Playground link to code