如何在 TypeScript 中编写泛型类型谓词?
在以下示例中,if (shape.kind == 'circle')
不会将类型缩小到 Shape<'circle'>
/Circle
/{ kind: 'circle', radius: number }
interface Circle {
kind: 'circle';
radius: number;
}
interface Square {
kind: 'square';
size: number;
}
type Shape<T = string> = T extends 'circle' | 'square'
? Extract<Circle | Square, { kind: T }>
: { kind: T };
declare const shape: Shape;
if (shape.kind == 'circle') shape.radius;
// error TS2339: Property 'radius' does not exist on type '{ kind: string; }'.
我尝试编写一个泛型类型谓词来解决这个问题,但以下不起作用,因为 the type parameter isn't available at runtime
function isShape1<T extends string>(shape: Shape): shape is Shape<T> {
return shape.kind extends T;
}
以下确实有效,但前提是类型参数
T
是文字(在编译和运行时具有相同的值)function isShape2<T extends string>(shape: Shape, kind: T): shape is Shape<T> {
return shape.kind == kind;
}
if (isShape2(shape, 'circle')) shape.radius; // Works ✓
declare const kind: string;
if (!isShape2(shape, kind)) shape.kind;
// error TS2339: Property 'kind' does not exist on type 'never'.
更新 1
@jcalz 麻烦是我需要
declare const kind: string;
if (kind != 'circle' && kind != 'square') shape = { kind };
去工作。正如您所指出的,我想使用有区别的工会,但不能。如果它是一个有区别的联合,你能写一个泛型类型谓词吗?
type Shape<T = string> = Extract<Circle | Square, { kind: T }>;
以下仍然仅在类型参数是文字时才有效
function isShape3<T extends Shape['kind']>(shape: Shape, kind: T): shape is Shape<T> {
return shape.kind == kind;
}
if (isShape3(shape, 'circle')) shape.radius; // Works ✓
declare const kind: Shape['kind']; // 'circle' | 'square'
if (!isShape3(shape, kind)) shape.kind;
// error TS2339: Property 'kind' does not exist on type 'never'.
唯一的区别是在这种情况下编译器已经提供了一个工作类型谓词
if (shape.kind != kind) shape.kind; // Works ✓
更新 2
@jcalz 在运行时它可以做与
shape.kind == kind
相同的事情吗? ?这是一个更简洁的演示
declare const s: string;
declare const kind: 'circle' | 'square';
declare let shape: 'circle' | 'square';
if (s == kind) shape = s; // Works ✓
if (shape != kind) shape.length; // Works ✓
function isShape1(s: string, kind: 'circle' | 'square') {
return s == kind;
}
if (isShape1(s, kind)) shape = s;
// error TS2322: Type 'string' is not assignable to type '"square" | "circle"'.
// https://github.com/microsoft/TypeScript/issues/16069
function isShape2(
s: string,
kind: 'circle' | 'square'
): s is 'circle' | 'square' {
return s == kind;
}
if (isShape2(s, kind)) shape = s; // Works ✓
if (!isShape2(shape, kind)) shape.length;
// error TS2339: Property 'length' does not exist on type 'never'.
更新 3
感谢@jcalz 和@KRyan 的深思熟虑的回答! @jcalz 的解决方案很有希望,特别是如果我不允许非缩小的情况,而不是仅仅解除它(通过重载)。
但是它仍然受制于 problem you point out (Number.isInteger(),坏事发生了)。考虑以下示例
function isTriangle<
T,
K extends T extends K ? never : 'equilateral' | 'isosceles' | 'scalene'
>(triangle: T, kind: K): triangle is K & T {
return triangle == kind;
}
declare const triangle: 'equilateral' | 'isosceles' | 'scalene';
declare const kind: 'equilateral' | 'isosceles';
if (!isTriangle(triangle, kind)) {
switch (triangle) {
case 'equilateral':
// error TS2678: Type '"equilateral"' is not comparable to type '"scalene"'.
}
}
triangle
永远不会小于 kind
所以!isTriangle(triangle, kind)
永远不会是never
,感谢条件类型 (👍),但它仍然比应有的窄(除非 K
是文字)。更新 4
再次感谢 @jcalz 和 @KRyan 耐心地解释这实际上是如何实现的,以及随之而来的弱点。我选择了@KRyan 的答案来贡献假名义的想法,尽管您的综合答案非常有帮助!
我的结论是
s == kind
的类型(或 triangle == kind
或 shape.kind == kind
)是内置的,但(尚未)可供用户使用,以分配给其他事物(如谓词)。我不确定这是否与 one-sided type guards 完全相同b/c
s == kind
的假分支在(一种)情况下确实缩小了declare const triangle: 'equilateral' | 'isosceles' | 'scalene';
if (triangle != 'scalene')
const isosceles: 'equilateral' | 'isosceles' = triangle;
为了更好地激发这个问题
string | number
,允许扩展)。因此,内置的 rr.rdtype == 'RRSIG'
行为不适用。除非我首先使用用户定义的类型保护 (isTypedRR(rr) && rr.rdtype == 'RRSIG'
) 将其缩小为真正的可区分联合,否则这不是一个糟糕的选择。 function isRRSIG(rr): rr is RR<'RRSIG'>
, function isDNSKEY(rr): rr is RR<'DNSKEY'>
等)。可能这就是我将继续做的事情:重复但显而易见。 s == kind
/rr.rdtype == rdtype
不同)。例如function isRR<T>(rr, rdtype: T): rr is RR<T>
.因此这个问题。 这使我无法说包装
isTypedRR(rr) && rr.rdtype == rdtype
在 function isRR(rr, rdtype)
.谓词内 rr
被合理缩小,但唯一的选择是(当前)rr is RR<T>
(或者现在是假名)。也许当type guards are inferred ,合理地缩小谓词之外的类型也很简单吗?或者当types can be negated ,给定一个不可枚举的判别式,就有可能建立一个真正的判别联合。我希望
s == kind
的类型(更方便:-P)可供用户使用。再次感谢!
最佳答案
关于缩小条件类型
所以从根本上说,你的问题是缩小一个值并不会为了映射或条件类型而缩小它的类型。见 this issue on the GitHub bug tracker ,特别是 this comment解释为什么这不起作用:
If I've read correctly, I think this is working as intended; in the general case, the type of
foobar
itself doesn't necessarily reflect thatFooBar
(the type variable) will describe identical types of a given instantiation. For example:function compare<T>(x: T, y: T) { if (typeof x === "string") { y.toLowerCase() // appropriately errors; 'y' isn't suddenly also a 'string' } // ... } // why not? compare<string | number>("hello", 100);
使用 type-guards 可以帮助您实现这一目标:
interface Circle {
kind: 'circle';
radius: number;
}
interface Square {
kind: 'square';
size: number;
}
type Shape<T = string> = T extends 'circle' | 'square'
? Extract<Circle | Square, { kind: T }>
: { kind: T };
declare const s: string;
declare let shape: Shape;
declare function isShapeOfKind<Kind extends string>(
shape: Shape,
kind: Kind,
): shape is Shape<Kind>;
if (s === 'circle' && isShapeOfKind(shape, s)) {
shape.radius;
}
else if (s === 'square' && isShapeOfKind(shape, s)) {
shape.size;
}
else {
shape.kind;
}
但是你必须检查 s
的类型在您可以使用 isShapeOfKind
之前并期望它起作用。那是因为在检查 s === 'circle'
之前或 s === 'square'
, s
的类型是 string
, 所以你得到的推论 isShapeOfKind<string>(shape, s)
这只告诉我们shape is Shape<string>
我们已经知道了(错误的情况是 never
因为 shape
被定义为 Shape
,即 Shape<string>
——它永远不会不是)。你希望发生的事情(但 Typescript 不做的事情)是让它变成类似 Shape<typeof s>
的东西。然后作为关于 s
的更多信息下定决心,了解shape
决心,决意,决定。 Typescript 不会跟踪可能相互关联的单独变量的类型。你可以这样做的另一种方法是让事物不是一个单独的变量,如果你真的必须这样做的话。也就是说,定义几个接口(interface),如
interface ShapeMatchingKind<Kind extends string> {
shape: Shape<Kind>;
kind: Kind;
}
interface ShapeMismatchesKind<ShapeKind extends string, Kind extends string> {
shape: Shape<ShapeKind>;
kind: Kind;
}
type ShapeAndKind = ShapeMatchingKind<string> | ShapeMismatchesKind<string, string>;
declare function isShapeOfKind(
shapeAndKind: ShapeAndKind,
): shapeAndKind is ShapeMatchingKind<string>;
const shapeAndKind = { shape, kind: s };
if (isShapeOfKind(shapeAndKind)) {
const pretend = shapeAndKind as ShapeMatchingKind<'circle'> | ShapeMatchingKind<'square'>;
switch (pretend.kind) {
case 'circle':
pretend.shape.radius;
break;
case 'square':
pretend.shape.size;
break;
default:
shapeAndKind.shape.kind;
break;
}
}
但是,即使在这里,您也必须使用 pretend
技巧——将变量转换为更窄的类型,然后当 pretend
是 never
,你知道原来的变量实际上不是那个更窄类型的一部分。此外,较窄的类型必须是 ShapeMatchesKind<A> | ShapeMatchesKind<B> | ShapeMatchesKind<C>
而不是 ShapeMatchesKind<A | B | C>
因为一个 ShapeMatchesKind<A | B | C>
可能有 shape: Shape<A>
和 kind: C
. (如果你有一个联合 A | B | C
,你可以使用条件类型来实现你需要的分布式版本。)在我们的代码中,我们结合了
pretend
经常与 otherwise
:function otherwise<R>(_pretend: never, value: R): R {
return value;
}
otherwise
的优势|是你可以写你的default
像这样的情况:default:
otherwise(pretend, shapeAndKind.shape.kind);
break;
现在otherwise
将要求 pretend
是 never
— 确保您的 switch 语句涵盖了 pretend
中的所有可能性的窄型。如果您添加了要专门处理的新形状,这将非常有用。您不必使用
switch
显然,这里; if
的链子/else if
/else
将以同样的方式工作。关于不完美的字体保护
在您的最后一次迭代中,您的问题是
isTriangle
返回 false
至typeof triangle & typeof kind
什么时候真的是false
是 triangle
的值吗?和 kind
的值不符合。因此,您会遇到 Typescript 同时看到 'equilateral'
的情况。和 'isosceles'
排除,因为typeof kind
是 'equilateral' | 'isosceles'
但是 kind
的实际值(value)只是这两件事之一。您可以通过 fake nominal types 解决此问题,所以你可以做类似的事情
class MatchesKind { private 'matches some kind variable': true; }
declare function isTriangle<T, K>(triangle: T, kind: K): triangle is T & K & MatchesKind;
declare const triangle: 'equilateral' | 'isosceles' | 'scalene';
declare const kind: 'equilateral' | 'isosceles';
if (!isTriangle(triangle, kind)) {
switch (triangle) {
case 'equilateral': 'OK';
}
}
else {
if (triangle === 'scalene') {
// ^^^^^^^^^^^^^^^^^^^^^^
// This condition will always return 'false' since the types
// '("equilateral" & MatchesKind) | ("isosceles" & MatchesKind)'
// and '"scalene"' have no overlap.
'error';
}
}
请注意,我使用了 if
这里— switch
由于某种原因似乎不起作用,它允许 case 'scalene'
在第二 block 没有投诉,即使 triangle
的类型那时应该使这不可能。然而 ,这似乎是一个非常非常糟糕的设计。这可能只是假设的插图场景,但我真的很难确定您为什么要以这种方式设计事物。完全不清楚您为什么要检查
triangle
与 kind
的值相反并使结果出现在类型域中,但不缩小 kind
以至于您实际上可以知道它的类型(因此是 triangle
的)。最好缩小kind
首先,然后用它来缩小 triangle
——在那种情况下,你没有问题。你似乎在某个地方颠倒了一些逻辑,我认为 Typescript 对此感到很不自在。我当然是。
关于TypeScript 泛型类型谓词,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57750277/