可以使用TypeGuard缩小一个泛型的范围吗?

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

possible to narrow a generic with typeguard?

问题

我正在尝试缩小一个通用类型,其中自动完成会捕捉到它正在命中一个类型保护,因此不会再次到达同一段代码。我认为原因是我没有将类型限制为通用类型,但我不确定如何做到这一点,这是否可能?感觉应该是可能的,但我不确定。任何帮助将不胜感激。

// 设置
export type Feature<Geometry> = {
  type: 'Feature',
  geometry: Geometry
}

type Geometry = Point | Curve

interface Base {
  type: string
}

interface Point extends Base {
  type: 'Point'
}

interface Curve extends Base {
  type: 'Curve'
}

// 类型保护
function isGeometry<G extends Geometry, U extends G['type']>(geometry: G, discriminator: U): geometry is Extract<G, { type: U }> {
  return geometry.type === discriminator
}

function isFeature<G extends Geometry, U extends G['type']>(feature: Feature<G>, discriminator: U): feature is Feature<Extract<G, { type: U }>> {
  return feature.geometry.type === discriminator
}

function whatGeometry(feature: Feature<Point | Curve>) {
  if (isGeometry(feature.geometry, 'Curve')) {
    return feature.geometry;
  }
  if (isGeometry(feature.geometry, 'Point')) {
    return feature.geometry;
  } // 自动完成,并知道我们不能再有其他几何类型
  return;
}

function whatFeature(feature: Feature<Point | Curve>) {
  if (isFeature(feature, 'Curve')) {
    return feature.geometry;
  }
  if (isFeature(feature, 'Point')) {
    return feature;
  } // 假设我们可能有另一个 Feature<Point>,即使上面的类型保护应该捕获它
  return;
}

Playground

英文:

I'm trying to narrow a generic, where the autocomplete picks up on that it's hitting a typeguard, and thus won't reach that same block of code again. I think the reason is that I'm not constraining the type to the Generic, but im not sure how i would do that, is it even possible? It feels like it should be possible but im unsure. Any help would be appreciated.

// Setup
export type Feature&lt;Geometry&gt; = {
type: &#39;Feature&#39;,
geometry: Geometry
}
type Geometry = Point | Curve
interface Base {
type: string
}
interface Point extends Base{
type: &#39;Point&#39;
}
interface Curve extends Base {
type: &#39;Curve&#39;
}
// Typeguard
function isGeometry&lt;G extends Geometry, U extends G[&#39;type&#39;]&gt;(geometry: G, disciminator: U): geometry is Extract&lt;G, {type: U}&gt;{
return geometry.type === disciminator
}
function isFeature&lt;G extends Geometry, U extends G[&#39;type&#39;]&gt;(feature: Feature&lt;G&gt;, disciminator: U): feature is Feature&lt;Extract&lt;G, {type: U}&gt;&gt; {
return feature.geometry.type === disciminator
}
function whatGeometry(feature: Feature&lt;Point | Curve&gt;) {
if(isGeometry(feature.geometry, &#39;Curve&#39;)){
return feature.geometry;
// ^?
}
if(isGeometry(feature.geometry, &#39;Point&#39;)){
return feature.geometry;
// ^?
} // Autocompletes, and knows that we can&#39;t have anything else for a geometry, 
return;
}
function whatFeature(feature: Feature&lt;Point | Curve&gt;) {
if(isFeature(feature, &#39;Curve&#39;)){
return feature.geometry;
// ^?
}
if(isFeature(feature, &#39;Point&#39;)) {
return feature;
// ^?
} // Assumes we can have another Feature&lt;Point&gt; even though the upper typeguard should have caught it
return;
}

Playground

答案1

得分: 1

以下是您要翻译的内容:

主要问题是,像 isFeature 这样的用户自定义类型保护函数仅允许您精确指定它们在返回 true 时应该发生什么,即将受保护的参数的类型缩小到受保护的类型。因此,对于返回类型为 arg is Type 的函数,我们知道对于 true 结果,arg 将被缩小为类似 typeof arg & TypeExtract<typeof arg, Type>(使用 Extract 实用程序类型),具体取决于 arg 的类型。粗略地说,如果 typeof arg联合类型,那么您将获得类似于 Extract 的行为;否则,您将获得 交集 类型的行为。(我在这里经常说“类似”,因为实际行为有点更微妙,我不想走得太远。)

但是,当函数返回 false 时,编译器必须自行确定要执行的操作。没有像在 microsoft/TypeScript#15048 中请求的“单边”或“细粒度”类型保护函数,可以让您说 arg is TrueType else FalseType 或类似的语句。目前发生的情况是,如果 arg 具有联合类型,那么您将获得类似于 Exclude<typeof arg, Type> 的结果,实际上会筛选 arg。但是如果 arg 没有联合类型,那么您只会得到 typeof arg。TypeScript 不支持 否定类型,如 not Type,因此在 false 情况下没有类似于 typeof arg & not Type 的类型,类似于 typeof arg & Type,可以缩小为。 (有一个否定类型的实现在 microsoft/TypeScript#29317 中,但从未被采纳。)因此,总体上编译器通常能够保持 arg 不变。

这直接导致了您在 isFeature 中看到的行为。输入类型 Feature<Point | Curve> 不是联合类型,因此当 isFeature 返回 false 时,编译器对输入类型的操作不多。

如果您想要在 false 分支上缩小类型,您应该考虑将输入类型设置为像 Feature<Point> | Feature<Curve> 这样的联合类型。这样做更复杂,因为您当前的 isFeature() 不接受这种类型的输入。但是您可以重构以允许联合输入,可能像这样:

function isFeature<T extends Geometry['type'], U extends T>(
  feature: Feature<Geometry & { type: T }>, discriminator: U
): feature is Feature<Geometry & { type: U }> {
  return feature.geometry.type === discriminator
}

function whatFeature(feature: Feature<Point> | Feature<Curve>) {
  if (isFeature(feature, 'Curve')) {
    return feature.geometry;
  }
  feature;
  if (isFeature(feature, 'Point')) {
    return feature;
  }
  return;
}

因此,featureFeature<Point> | Feature<Curve>,使得负类型保护结果可以缩小它。而且 isFeature()type T 中是 泛型,允许联合输入。当它返回 true 时,编译器会将输入缩小到其几何类型为 U 的那些联合成员,因此负结果会将输入缩小到这些成员的补集。

这为您提供了所需的行为,即在负检查 isFeature(feature, 'Curve') 后,feature 缩小为 Feature<Point>

英文:

The main issue you're facing is that user-defined type guard functions like isFeature only let you specify exactly what should happen if they return true, which is to narrow the relevant argument's type to the guarded type. So for a function whose return type is arg is Type, we know that for a true result, arg will be narrowed to something like typeof arg &amp; Type, or Extract&lt;typeof arg, Type&gt; (using the Extract utility type), depending on what type arg has. Roughly, if typeof arg is a union type then you get the Extract-like behavior; otherwise you get the intersection-like behavior. (I'm saying "like" a lot here because the actual behavior is a bit more subtle and I don't want to digress too much.)

But when the function returns false, the compiler has to try to determine what to do by itself. There are no "one-sided" or "fine-grained" type guard functions as requested in microsoft/TypeScript#15048 that would let you say arg is TrueType else FalseType or the like. Right now what happens is, if arg has a union type, then you get something like Exclude&lt;typeof arg, Type&gt;, which actually filters arg. But if arg does not have a union type, then you just get typeof arg. TypeScript lacks negated types like not Type, so there is no type like typeof arg &amp; not Type analogous to typeof arg &amp; Type that you can narrow to in the false case. (There was an implementation of negated types at microsoft/TypeScript#29317 but it was never adopted.) So the best the compiler can do in general is to leave arg alone.


That leads directly to the behavior you're seeing with isFeature. The input type Feature&lt;Point | Curve&gt; is not a union type, so when isFeature returns false, there's not much the compiler can do to the type of the input.

If you want to be able to narrow on the false branch, you should consider making the input type a union like Feature&lt;Point&gt; | Feature&lt;Curve&gt;. It's more complicated to do this, since your current isFeature() would balk at an input of that type. But you could refactor to allow union inputs, possibly like this:

function isFeature&lt;T extends Geometry[&#39;type&#39;], U extends T&gt;(
feature: Feature&lt;Geometry &amp; { type: T }&gt;, disciminator: U
): feature is Feature&lt;Geometry &amp; { type: U }&gt; {
return feature.geometry.type === disciminator
}
function whatFeature(feature: Feature&lt;Point&gt; | Feature&lt;Curve&gt;) {
if (isFeature(feature, &#39;Curve&#39;)) {
return feature.geometry;
// ^?(parameter) feature: Feature&lt;Curve&gt;
}
feature;
// ^?(parameter) feature: Feature&lt;Point&gt;
if (isFeature(feature, &#39;Point&#39;)) {
return feature;
//     ^?(parameter) feature: Feature&lt;Point&gt;
}
return;
} 

So feature is Feature&lt;Point&gt; | Feature&lt;Curve&gt;, making it possible for a negative type guard result to narrow it. And isFeature() is generic in the type T which allows union inputs. When it returns true, then the compiler narrows the input to just those union members whose geometry type is U, and so a false result narrows the input to the complement of those members.

That gives you the behavior you want, where feature is narrowed to Feature&lt;Point&gt; after a negative check for isFeature(feature, &#39;Curve&#39;), as desired.

Playground link to code

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

发表评论

匿名网友

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

确定