类型检查,确保值不是其中之一。

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

type checking that value is not one of several values

问题

在 TypeScript 中,你可以使用枚举作为类型,以确保变量的值在枚举中。例如:

type children = "Andy" | "Bob";

export const func = (name: children): void => {
...
};

然而,我希望有一种功能来确保变量的值不在枚举(或任何其他类型的集合)中。我知道可以通过简单的 if 语句/for 循环来实现这一点,但我希望当变量不是某些值之一时,会抛出类似类型错误的东西。是否有一种类似于枚举的反向功能,可以用来检查变量名是否不是几个特定值之一?在上面的示例中,我希望确保 name 的值不是 AndyBob

英文:

In typescript you can use an enum as a type in order to ensure that a variable has a value that is within the enum. For example:

type children = "Andy" | "Bob"

export const func = (name: children) : void => {
...
};

However, I wish for functionality that ensures the value of a variable is NOT within an enum (or any other kind of collection). I know this can be achieved with simple if statements/for loops but I'm hoping that when the variable is not one of certain values, something like a type error will be thrown. Is there something like the opposite of an enum that I can use to check a variable name is NOT one of several values? In the above example, I would want it so that the value of name is ensured not to be Andy or Bob.

答案1

得分: 2

TypeScript缺乏否定类型(类似于microsoft/TypeScript#29317中实现的类型,但从未合并到语言中),因此没有与"所有不是Childrenstring"对应的特定类型。如果有的话,你可以写成type NotChildren = string & not Children,然后只需使用(name: NotChildren) => {}。相反,我们将需要通过使用泛型来模拟它们。

我将采取的方法是观察泛型类型T,该类型已经受到了约束,只能为string时,只有当TChildren之间完全没有重叠时,才应拒绝它。也就是说,TChildren交集应该是空的/不可能的never类型。因此,让我们使funcT上具有泛型,并且给name赋予条件类型(T & Children) extends never ? T : never,如果T & Children没有重叠,则评估为T,否则为never

type Children = "Andy" | "Bob";
export const func = <T extends string>(
  name: (T & Children) extends never ? T : never
): void => { };

基本用法如下:

func("Andy"); // error;
// const func: <"Andy">(name: never) => void

func("Bob"); // error
// const func: <"Bob">(name: never) => void

func("Charie"); // okay; 
// const func: <"Charie">(name: "Charie") => void

前两个调用失败,因为当T被推断为文字类型"Andy""Bob"时,T & Children是可能的,因此(T & Children) extends never为false,因此(T & Children) extends never ? T : nevernever,因此拒绝了这些参数。

第二个调用成功,因为当T被推断为文字类型"Charlie"时,T & Children是不可能的,因此(T & Children) extends never为true,因此(T & Children) extends never ? T : neverT,因此接受了参数。

到目前为止看起来很好。


现在让我们考虑一些更复杂的情况:

func(Math.random() < 0.5 ? "Andy" : "Charlie"); // error
// const func: <"Andy" | "Charlie">(name: never) => void

这失败了,因为参数要么是"Andy",要么是"Charlie",并且因为Children与该类型之间存在一些重叠(即"Andy"),所以它被拒绝了。

任意的string也被拒绝:

function foo(x: string) {
  func(x); // error, maybe it's "Andy" or "Bob"
  // const func: <string>(name: never) => void
}

因为stringChildren之间存在重叠(即Children),所以它被拒绝了。请注意,编译器和人类对于什么构成“任意”字符串可能有不同的看法。如果使用加号运算符来连接字符串,编译器不会尝试弄清楚可能是什么,而只会将其扩展为string。因此,这被拒绝了:

func("Char" + "lie"); // error 
// const func: <string>(name: never) => void

我们知道这是"Charlie",但编译器不知道,所以不能接受它。

另一方面,以下调用被接受

function bar(x: `Char${string}`) {
  func(x); // okay, no way it can be "Andy" or "Bob"
  // const func: <`Char${string}`>(name: `Char${string}`) => void
}

模式模板文字类型(如ms/TS#40598中实现的那样)`Char${string}`表示“以'Char'开头的任何字符串”。编译器了解到这种类型与Children之间没有重叠,因此可以工作。

相反,以下调用失败:

function baz(x: `An${string}`) {
  func(x); // error
  // const func: <`An${string}`>(name: never) => void
}

因为“以'An'开头的字符串”与Children之间存在重叠,即"Andy"

因此,基本上,在怀疑时,编译器会拒绝调用。这可能会使其有点难以使用,因为不接受任意的string输入,但这可能是你想要的!

英文:

TypeScript lacks negated types (of the sort implemented in microsoft/TypeScript#29317 but never merged into the language), so there's no specific type corresponding to "all strings that are not Children". If there were you could write type NotChildren = string &amp; not Children and then just have (name: NotChildren) =&gt; {}. Instead, we'll need to simulate them by using generics.

The approach I'll take here is to observe that a generic type T that's been constrained to string should be rejected if and only if there's no overlap whatsoever between T and Children. That is, the intersection of T and Children should be the empty/impossible never type. So let's make func generic in T and give name the conditional type (T &amp; Children) extends never ? T : never, which evaluates to T if T &amp; Children has no overlap, and never otherwise:

type Children = &quot;Andy&quot; | &quot;Bob&quot;;
export const func = &lt;T extends string&gt;(
  name: (T &amp; Children) extends never ? T : never
): void =&gt; { };

The basic usage looks like this:

func(&quot;Andy&quot;); // error;
// const func: &lt;&quot;Andy&quot;&gt;(name: never) =&gt; void

func(&quot;Bob&quot;); // error
// const func: &lt;&quot;Bob&quot;&gt;(name: never) =&gt; void

func(&quot;Charie&quot;); // okay; 
// const func: &lt;&quot;Charie&quot;&gt;(name: &quot;Charie&quot;) =&gt; void

The first two calls fail because when T is inferred to be the literal types &quot;Andy&quot; or &quot;Bob&quot;, then T &amp; Children is possible, so (T &amp; Children) extends never is false, and so (T &amp; Children) extends never ? T : never is never, and so the arguments are rejected.

The second call succeeds because when T is inferred to be literal type &quot;Charlie&quot;, then T &amp; Children is impossible, so (T &amp; Children) extends never is true, and so (T &amp; Children) extends never ? T : never is T, and so the argument is accepted.

So that looks good, so far.


Now for some more complicated situations:

func(Math.random() &lt; 0.5 ? &quot;Andy&quot; : &quot;Charlie&quot;); // error
// const func: &lt;&quot;Andy&quot; | &quot;Charlie&quot;&gt;(name: never) =&gt; void

That fails because the argument is either &quot;Andy&quot; or &quot;Charlie&quot;, and since there is some overlap between Children and that type (namely &quot;Andy&quot;), then it is rejected.

Also rejected is an arbitrary string:

function foo(x: string) {
  func(x); // error, maybe it&#39;s &quot;Andy&quot; or &quot;Bob&quot;
  // const func: &lt;string&gt;(name: never) =&gt; void
}

because there's overlap between string and Children (namely, Children). Note that the compiler and a human being might have a different view of what constitutes an "arbitrary" string. If you use the + operator to concatenate strings, the compiler doesn't try to figure out what that might be, and just widens it to string. So this is rejected:

func(&quot;Char&quot; + &quot;lie&quot;); // error 
// const func: &lt;string&gt;(name: never) =&gt; void

We know that's &quot;Charlie&quot;, but the compiler doesn't, so it can't accept it.

On the other hand it accepts the following call:

function bar(x: `Char${string}`) {
  func(x); // okay, no way it can be &quot;Andy&quot; or &quot;Bob&quot;
  // const func: &lt;`Char${string}`&gt;(name: `Char${string}`) =&gt; void
}

The pattern template literal type (as implemented in ms/TS#40598) `Char${string}` means "any string starting with &quot;Char&quot;". The compiler understands that there is no overlap between such a type and Children, so it works.

Conversely the following fails:

function baz(x: `An${string}`) {
  func(x); // error
  // const func: &lt;`An${string}`&gt;(name: never) =&gt; void
}

because there is overlap between "strings that start with &quot;An&quot;" and Children, namely &quot;Andy&quot;.

So essentially, when in doubt, the compiler rejects the call. That could make it somewhat difficult to use, since arbitrary string inputs are not accepted, but that's presumably what you want!

Playground link to code

huangapple
  • 本文由 发表于 2023年7月28日 05:37:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/76783536.html
匿名

发表评论

匿名网友

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

确定