typescript - TypeScript 泛型类型谓词
问题描述
如何在 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; }'.
我尝试编写一个泛型类型谓词来解决这个问题,但以下不起作用,因为类型参数在运行时不可用
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 的解决方案很有希望,特别是如果我不允许非缩小的情况,而不是仅仅解除它(通过重载)。
但是,它仍然受制于您指出的问题(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
so !isTriangle(triangle, kind)
will never be 更窄never
,但它仍然比应有的更窄(除非K
是文字)。
更新 4
再次感谢 @jcalz 和 @KRyan 耐心地解释这实际上是如何实现的,以及随之而来的弱点。我选择了@KRyan 的答案来贡献假名义的想法,尽管您的综合答案非常有帮助!
我的结论是s == kind
(or triangle == kind
or shape.kind == kind
) 的类型是内置的,并且(尚未)可供用户使用,无法分配给其他事物(如谓词)。
我不确定这与单面型防护装置b/c 的错误分支s == kind
在(一种)情况下确实变窄的情况完全相同
declare const triangle: 'equilateral' | 'isosceles' | 'scalene';
if (triangle != 'scalene')
const isosceles: 'equilateral' | 'isosceles' = triangle;
为了更好地激发这个问题
- 我有一个几乎是可区分联合(DNS RRs)的类型,除了我无法枚举所有判别式的值(通常它是 a
string | number
,允许扩展)。因此,内置rr.rdtype == 'RRSIG'
行为不适用。除非我首先使用用户定义的类型保护 () 将其缩小为真正的可区分联合,否则isTypedRR(rr) && rr.rdtype == 'RRSIG'
这不是一个糟糕的选择。 - 我可以为我可以枚举的每个 RR 类型实现用户定义的类型保护,但这是很多重复(
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>
(或现在是假名义)。
也许当类型保护被推断出来时,合理地缩小谓词之外的类型也是微不足道的?或者当类型可以被否定时,给定一个不可枚举的判别式,就有可能建立一个真正的可区分联合。我确实希望s == kind
用户可以使用(更方便:-P)的类型。再次感谢!
解决方案
关于缩小条件类型
因此,从根本上讲,您的问题是缩小值不会为了映射或条件类型而缩小其类型。在 GitHub 错误跟踪器上查看此问题,特别是此评论解释了为什么这不起作用:
如果我没看错,我认为这是按预期工作的;在一般情况下,
foobar
本身的类型不一定反映FooBar
(类型变量)将描述给定实例的相同类型。例如: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'
or之前s === 'square'
,类型是s
is string
,所以你得到的推论isShapeOfKind<string>(shape, s)
只告诉我们shape is Shape<string>
我们已经知道的(错误的情况是never
因为shape
被定义为 a Shape
,即 a Shape<string>
——它永远不会不是一)。你想要发生的事情(但 Typescript 不做的事情)是让它变成类似的东西Shape<typeof s>
,然后随着更多信息s
的确定,有关的知识shape
也被确定。Typescript 不跟踪可能相互关联的单独变量的类型。
你可以这样做的另一种方法是让事物不是一个单独的变量,如果你真的必须这样做的话。也就是说,定义几个接口,如
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
is时never
,您知道原始变量实际上不是该更窄类型的一部分。此外,较窄的类型必须是ShapeMatchesKind<A> | ShapeMatchesKind<B> | ShapeMatchesKind<C>
而不是ShapeMatchesKind<A | B | C>
因为 aShapeMatchesKind<A | B | C>
可以具有shape: Shape<A>
and kind: C
。(如果你有一个 union 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
's 的缩小类型中的所有可能性。如果您添加了要专门处理的新形状,这将非常有用。
显然,您不必switch
在这里使用;一连串的if
//将以相同的方式工作else if
。else
关于不完美的字体保护
在您的最后一次迭代中,您的问题是isTriangle
返回false
到typeof triangle & typeof kind
真正false
的值和值不匹配triangle
的情况。因此,您会遇到一种情况,Typescript 同时看到并排除,因为was but的实际值只是这两件事之一。kind
'equilateral'
'isosceles'
typeof kind
'equilateral' | 'isosceles'
kind
你可以用假的名义类型来解决这个问题,所以你可以做类似的事情
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
- <code>switch 由于某种原因似乎不起作用,它允许case 'scalene'
在第二个块中没有任何抱怨,即使此时的类型triangle
应该使这不可能。
然而,这似乎是一个非常非常糟糕的设计。这可能只是假设的插图场景,但我真的很难确定您为什么要以这种方式设计事物。完全不清楚为什么要检查triangle
of 的值kind
并将结果显示在类型域中,但没有缩小kind
到您实际上可以知道它的类型(因此triangle
是 's)的程度。最好先缩小kind
,然后用它来缩小triangle
——在那种情况下,你没有问题。你似乎在某个地方颠倒了一些逻辑,我认为 Typescript 对此感到很不自在。我当然是。
推荐阅读
- java - 从摘要或校验和中检测目录(java 8)中更改的文件
- javascript - 反应组件只有一个显示数据
- php - AJAX 帖子编号作为字符串
- view - Rails 5.2.3 content_for 未按规定工作
- python - 如何为单个记录器使用多种格式
- loops - 由于可能不正确的比较,无法在 Assembly 中输入 for 循环
- c - 使用结构读取 PGM 文件
- javascript - 如何修复无法读取未定义的属性“纬度”
- laravel-dusk - 如何在 Laravel Dusk 浏览器测试、单元测试和功能测试之间共享代码
- python - 由于某种原因,我无法显示此文件