【发布时间】:2020-01-05 02:20:21
【问题描述】:
如何在 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;
为了更好地激发这个问题
- 我有一个类型,它几乎是一个可区分的联合 (DNS RR),但我无法枚举所有判别式的值(通常它是
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>(或者现在是假名义)。
也许当type guards are inferred 时,合理地缩小谓词之外的类型也是微不足道的?或者当types can be negated 时,给定一个不可枚举的判别式,就有可能建立一个真正的判别联合。我确实希望s == kind 的类型(更方便:-P)可供用户使用。再次感谢!
【问题讨论】:
-
我认为你的
Shape类型是问题,而不是类型保护......也许你想要type Shape<T extends (Circle | Square)["kind"] = (Circle | Square)["kind"]> = Extract<Circle | Square, { kind: T }>;...... 或者你应该只拥有type Shape = Circle | Square和类似type ShapeKind<T extends Shape["kind"]> = Extract<Shape, {kind: T}>并且不要尝试对这两件事都使用单一的类型名称。 ` -
另外,discriminated unions 仅在它们具有可在编译时区分联合的每个成员的判别属性时才有效。
{kind: string}类型不能算作有区别的联合,甚至不能算作联合,所以它不会表现得很好。如您的代码所示,使用类型保护检查{kind: string}与另一个{kind: string}只会使其保持不变(在“真”情况下)或将其缩小到never(在“假”情况下)。 -
????用户定义的类型保护在错误情况下与
==的工作方式不同(在真实情况下与!=不同)。如果守卫(x: T) => x is U返回false,则x的类型将缩小为Exclude<T, U>,而当x == y返回false时,x或y根本不会缩小。如果不适合缩小失败范围,请不要使用用户定义的类型保护(或bad things happen)。如果T和U是同一类型(例如"square" | "circle"),那么这种缩小会导致never,这是不好的。 -
或者,如果没有one-sided user defined type guards,我就无法按照您的意愿行事。而且我不知道我们是否会得到这些。
-
@jcalz 所以你做到了,我的错。看到这样一位 TS 专家这么说,我很惊讶,应该三重检查我的阅读是否正确。
标签: typescript