英文:
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;
}
英文:
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<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'
}
// Typeguard
function isGeometry<G extends Geometry, U extends G['type']>(geometry: G, disciminator: U): geometry is Extract<G, {type: U}>{
return geometry.type === disciminator
}
function isFeature<G extends Geometry, U extends G['type']>(feature: Feature<G>, disciminator: U): feature is Feature<Extract<G, {type: U}>> {
return feature.geometry.type === disciminator
}
function whatGeometry(feature: Feature<Point | Curve>) {
if(isGeometry(feature.geometry, 'Curve')){
return feature.geometry;
// ^?
}
if(isGeometry(feature.geometry, 'Point')){
return feature.geometry;
// ^?
} // Autocompletes, and knows that we can't have anything else for a geometry,
return;
}
function whatFeature(feature: Feature<Point | Curve>) {
if(isFeature(feature, 'Curve')){
return feature.geometry;
// ^?
}
if(isFeature(feature, 'Point')) {
return feature;
// ^?
} // Assumes we can have another Feature<Point> even though the upper typeguard should have caught it
return;
}
答案1
得分: 1
以下是您要翻译的内容:
主要问题是,像 isFeature
这样的用户自定义类型保护函数仅允许您精确指定它们在返回 true
时应该发生什么,即将受保护的参数的类型缩小到受保护的类型。因此,对于返回类型为 arg is Type
的函数,我们知道对于 true
结果,arg
将被缩小为类似 typeof arg & Type
或 Extract<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;
}
因此,feature
是 Feature<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 & Type
, or Extract<typeof arg, Type>
(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<typeof arg, Type>
, 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 & not Type
analogous to typeof arg & 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<Point | Curve>
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<Point> | Feature<Curve>
. 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<T extends Geometry['type'], U extends T>(
feature: Feature<Geometry & { type: T }>, disciminator: U
): feature is Feature<Geometry & { type: U }> {
return feature.geometry.type === disciminator
}
function whatFeature(feature: Feature<Point> | Feature<Curve>) {
if (isFeature(feature, 'Curve')) {
return feature.geometry;
// ^?(parameter) feature: Feature<Curve>
}
feature;
// ^?(parameter) feature: Feature<Point>
if (isFeature(feature, 'Point')) {
return feature;
// ^?(parameter) feature: Feature<Point>
}
return;
}
So feature
is Feature<Point> | Feature<Curve>
, 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<Point>
after a negative check for isFeature(feature, 'Curve')
, as desired.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论