如何在 TypeScript 中编写泛型类型谓词?
\n\n在下面的示例中,if (shape.kind == \'circle\')不会将类型缩小为Shape<\'circle\'>//Circle{ kind: \'circle\', radius: number }
interface Circle {\n kind: \'circle\';\n radius: number;\n}\n\ninterface Square {\n kind: \'square\';\n size: number;\n}\n\ntype Shape<T = string> = T extends \'circle\' | \'square\'\n ? Extract<Circle | Square, { kind: T }>\n : { kind: T };\n\ndeclare const shape: Shape;\nif (shape.kind == \'circle\') shape.radius;\n// error TS2339: Property \'radius\' does not exist on type \'{ kind: string; }\'.\nRun Code Online (Sandbox Code Playgroud)\n\n我尝试编写一个泛型类型谓词来解决此问题,但以下内容不起作用,因为类型参数在运行时不可用
\n\nfunction isShape1<T extends string>(shape: Shape): shape is Shape<T> {\n return shape.kind extends T;\n}\nRun Code Online (Sandbox Code Playgroud)\n\n以下确实有效,但前提是类型参数T是文字(在编译和运行时具有相同的值)
function isShape2<T extends string>(shape: Shape, kind: T): shape is Shape<T> {\n return shape.kind == kind;\n}\n\nif (isShape2(shape, \'circle\')) shape.radius; // Works \xe2\x9c\x93\n\ndeclare const kind: string;\nif (!isShape2(shape, kind)) shape.kind;\n// error TS2339: Property \'kind\' does not exist on type \'never\'.\nRun Code Online (Sandbox Code Playgroud)\n\n@jcalz 问题是我需要
\n\ndeclare const kind: string;\nif (kind != \'circle\' && kind != \'square\') shape = { kind };\nRun Code Online (Sandbox Code Playgroud)\n\n上班。正如您所指出的,我想使用受歧视的工会,但不能。如果它是一个可区分联合,您可以编写一个泛型类型谓词吗?
\n\ntype Shape<T = string> = Extract<Circle | Square, { kind: T }>;\nRun Code Online (Sandbox Code Playgroud)\n\n仅当类型参数是文字时,以下仍然有效
\n\nfunction isShape3<T extends Shape[\'kind\']>(shape: Shape, kind: T): shape is Shape<T> {\n return shape.kind == kind;\n}\n\nif (isShape3(shape, \'circle\')) shape.radius; // Works \xe2\x9c\x93\n\ndeclare const kind: Shape[\'kind\']; // \'circle\' | \'square\'\nif (!isShape3(shape, kind)) shape.kind;\n// error TS2339: Property \'kind\' does not exist on type \'never\'.\nRun Code Online (Sandbox Code Playgroud)\n\n唯一的区别是在这种情况下编译器已经提供了一个工作类型谓词
\n\nif (shape.kind != kind) shape.kind; // Works \xe2\x9c\x93\nRun Code Online (Sandbox Code Playgroud)\n\n@jcalz 在运行时它可以做同样的事情吗shape.kind == kind?
这是一个更简洁的演示
\n\ndeclare const s: string;\ndeclare const kind: \'circle\' | \'square\';\ndeclare let shape: \'circle\' | \'square\';\n\nif (s == kind) shape = s; // Works \xe2\x9c\x93\nif (shape != kind) shape.length; // Works \xe2\x9c\x93\n\nfunction isShape1(s: string, kind: \'circle\' | \'square\') {\n return s == kind;\n}\n\nif (isShape1(s, kind)) shape = s;\n// error TS2322: Type \'string\' is not assignable to type \'"square" | "circle"\'.\n// https://github.com/microsoft/TypeScript/issues/16069\n\nfunction isShape2(\n s: string,\n kind: \'circle\' | \'square\'\n): s is \'circle\' | \'square\' {\n return s == kind;\n}\n\nif (isShape2(s, kind)) shape = s; // Works \xe2\x9c\x93\nif (!isShape2(shape, kind)) shape.length;\n// error TS2339: Property \'length\' does not exist on type \'never\'.\nRun Code Online (Sandbox Code Playgroud)\n\n感谢@jcalz 和@KRyan 的深思熟虑的回答!@jcalz\ 的解决方案很有希望,特别是如果我不允许非缩小的情况,而不是仅仅解除它(通过过载)。
\n\n然而,它仍然受到您指出的问题的影响(Number.isInteger(),不好的事情发生)。考虑下面的例子
\n\nfunction isTriangle<\n T,\n K extends T extends K ? never : \'equilateral\' | \'isosceles\' | \'scalene\'\n>(triangle: T, kind: K): triangle is K & T {\n return triangle == kind;\n}\n\ndeclare const triangle: \'equilateral\' | \'isosceles\' | \'scalene\';\ndeclare const kind: \'equilateral\' | \'isosceles\';\n\nif (!isTriangle(triangle, kind)) {\n switch (triangle) {\n case \'equilateral\':\n // error TS2678: Type \'"equilateral"\' is not comparable to type \'"scalene"\'.\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n\ntriangle由于条件类型 () 的存在, will never be 比kindso !isTriangle(triangle, kind)will never be更窄,但它仍然比应有的更窄(除非是文字)。neverK
再次感谢 @jcalz 和 @KRyan 耐心地解释了这实际上是如何实现的,以及随之而来的弱点。我选择了 @KRyan 的答案来贡献假名义想法,尽管您的综合答案非常有帮助!
\n\ns == kind我的结论是(ortriangle == kind或)的类型shape.kind == kind是内置的,但用户(尚)无法将其分配给其他事物(例如谓词)。
我不确定这与单面类型防护完全相同, b/c 的错误分支s == kind在(一种)情况下会缩小
declare const triangle: \'equilateral\' | \'isosceles\' | \'scalene\';\nif (triangle != \'scalene\')\n const isosceles: \'equilateral\' | \'isosceles\' = triangle;\nRun Code Online (Sandbox Code Playgroud)\n\n首先为了更好地激发这个问题
\n\nstring | 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>(或现在是假名词)。
也许当推断出类型保护时,合理地缩小谓词之外的类型也很简单?或者,当类型可以被否定时,给定一个不可枚举的判别式,就有可能建立一个真正的判别联合。我确实希望s == kind用户可以使用这种类型(更方便:-P)。再次感谢!
所以从根本上来说,这里的问题是,为了映射或条件类型,缩小值并不会缩小其类型。请参阅GitHub bug tracker 上的此问题,特别是此评论解释了为什么此方法不起作用:
\n\n\n如果我没看错的话,我认为这正在按预期工作;在一般情况下,其本身的类型
\nfoobar不一定反映FooBar(类型变量)将描述给定实例化的相同类型。例如:Run Code Online (Sandbox Code Playgroud)\nfunction compare<T>(x: T, y: T) {\n if (typeof x === "string") {\n y.toLowerCase() // appropriately errors; \'y\' isn\'t suddenly also a \'string\'\n }\n // ...\n}\n\n// why not?\ncompare<string | number>("hello", 100);\n
使用类型保护可以帮助您实现这一目标:
\ninterface Circle {\n kind: \'circle\';\n radius: number;\n}\n\ninterface Square {\n kind: \'square\';\n size: number;\n}\n\ntype Shape<T = string> = T extends \'circle\' | \'square\'\n ? Extract<Circle | Square, { kind: T }>\n : { kind: T };\n\ndeclare const s: string;\ndeclare let shape: Shape;\n\ndeclare function isShapeOfKind<Kind extends string>(\n shape: Shape,\n kind: Kind,\n): shape is Shape<Kind>;\n\nif (s === \'circle\' && isShapeOfKind(shape, s)) {\n shape.radius;\n}\nelse if (s === \'square\' && isShapeOfKind(shape, s)) {\n shape.size;\n}\nelse {\n shape.kind;\n}\nRun Code Online (Sandbox Code Playgroud)\ns但在使用之前你必须检查它的类型isShapeOfKind并期望它能够工作。\xe2\x80\x99s 因为在检查s === \'circle\'or之前s === \'square\', 的类型s是string,所以你得到的推论isShapeOfKind<string>(shape, s)只告诉我们shape is Shape<string>我们已经知道的内容(错误的情况是never因为shape被定义为 a Shape,即Shape<string>\xe2\x80\x94 它永远不会不是一个)。你\xe2\x80\x99d想要发生的事情(但是Typescript不\xe2\x80\x99t做的事情)是让它变成类似的东西Shape<typeof s>,然后随着更多信息的s确定,关于的知识shape的确定。Typescript 不会跟踪可能彼此相关的单独变量的类型。
另一种方法是,如果确实需要的话,可以让事物不再是一个单独的变量。也就是说,定义几个接口,例如
\ninterface ShapeMatchingKind<Kind extends string> {\n shape: Shape<Kind>;\n kind: Kind;\n}\n\ninterface ShapeMismatchesKind<ShapeKind extends string, Kind extends string> {\n shape: Shape<ShapeKind>;\n kind: Kind;\n}\n\ntype ShapeAndKind = ShapeMatchingKind<string> | ShapeMismatchesKind<string, string>;\n\ndeclare function isShapeOfKind(\n shapeAndKind: ShapeAndKind,\n): shapeAndKind is ShapeMatchingKind<string>;\n\nconst shapeAndKind = { shape, kind: s };\nif (isShapeOfKind(shapeAndKind)) {\n const pretend = shapeAndKind as ShapeMatchingKind<\'circle\'> | ShapeMatchingKind<\'square\'>;\n switch (pretend.kind) {\n case \'circle\':\n pretend.shape.radius;\n break;\n case \'square\':\n pretend.shape.size;\n break;\n default:\n shapeAndKind.shape.kind;\n break;\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n但即使在这里,您也必须使用pretend技巧 \xe2\x80\x94a 版本的变量转换为较窄的类型,然后当pretendis 时never,您知道原始变量实际上是 \xe2\x80\x99t的较窄类型的一部分类型。此外,较窄的类型必须是ShapeMatchesKind<A> | ShapeMatchesKind<B> | ShapeMatchesKind<C>而不是ShapeMatchesKind<A | B | C>因为 aShapeMatchesKind<A | B | C>可以有shape: Shape<A>和kind: C。(如果您有 union A | B | C,则可以使用条件类型实现所需的分布式版本。)
在我们的代码中,我们pretend经常结合otherwise:
function otherwise<R>(_pretend: never, value: R): R {\n return value;\n}\nRun Code Online (Sandbox Code Playgroud)\n优点otherwise是你可以default这样写你的案例:
default:\n otherwise(pretend, shapeAndKind.shape.kind);\n break;\nRun Code Online (Sandbox Code Playgroud)\n现在otherwise要求pretend是never\xe2\x80\x94 确保你的 switch 语句涵盖了 \xe2\x80\x99s 缩小类型中的所有可能性pretend。如果您添加想要专门处理的新形状,这非常有用。
switch显然,你不必在这里使用\xe2\x80\x99 ;if//链将以同样的方式工作else if。else
在您的最终迭代中,您的问题是isTriangle返回false到typeof triangle & typeof kind真正的情况false是 的值和triangle的值不kind匹配。所以你会遇到这样一种情况,Typescript 认为\'equilateral\'和\'isosceles\'都被排除了,因为typeof kind是\'equilateral\' | \'isosceles\'但是kind\xe2\x80\x99s 的实际值只是这两件事之一。
您可以使用假名义类型来解决这个问题来解决这个问题,因此您可以执行类似的操作
\nclass MatchesKind { private \'matches some kind variable\': true; }\n\ndeclare function isTriangle<T, K>(triangle: T, kind: K): triangle is T & K & MatchesKind;\n\ndeclare const triangle: \'equilateral\' | \'isosceles\' | \'scalene\';\ndeclare const kind: \'equilateral\' | \'isosceles\';\n\nif (!isTriangle(triangle, kind)) {\n switch (triangle) {\n case \'equilateral\': \'OK\';\n }\n}\nelse {\n if (triangle === \'scalene\') {\n// ^^^^^^^^^^^^^^^^^^^^^^\n// This condition will always return \'false\' since the types\n// \'("equilateral" & MatchesKind) | ("isosceles" & MatchesKind)\'\n// and \'"scalene"\' have no overlap.\n \'error\';\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n请注意,我if在这里使用\xe2\x80\x94switch不\xe2\x80\x99t 似乎出于某种原因工作,它允许case \'scalene\'在第二个块中没有抱怨,即使类型triangle此时的类型应该使之成为不可能。
然而,这似乎是一个非常非常糟糕的设计。这可能只是假设的插图场景,但我真的很难确定为什么你想以这种方式设计东西。它\xe2\x80\x99 根本不清楚为什么你想要检查triangle的值并使kind结果出现在类型域中,但又没有缩小kind到你实际上可以知道其类型的程度(因此triangle\xe2\ x80\x99s)。最好kind先缩小,然后再用它来缩小triangle\xe2\x80\x94 在这种情况下,你没有问题。你似乎在某个地方颠倒了一些逻辑,而 Typescript 是 \xe2\x80\x94 合理的,我认为 \xe2\x80\x94 对此感到不舒服。我当然是。
| 归档时间: |
|
| 查看次数: |
8503 次 |
| 最近记录: |