如何在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 };
我希望能够使用区分联合(discriminated union)来工作,但正如你所指出的那样,我不能这样做。如果它是一个区分联合,你能写一个通用类型谓词吗?
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
更窄,因此!isTriangle(triangle,kind)
永远不会是never
,这要归功于条件类型(),但是它仍然比应该更窄(除非K
是文字)。
更新4
再次感谢@jcalz和@KRyan耐心解释如何实现此目标以及随之而来的弱点。我选择了@KRyan的答案,因为他提出了虚名思想,尽管您们的综合答案非常有帮助!
我的收获是s == kind
(或triangle == kind
或shape.kind == kind
)的类型是内置的,用户无法将其分配给其他内容(例如谓词)。
我不确定这是否与单侧类型保护完全相同,因为s == kind
的false分支在某种情况下会缩小。
declare const triangle: 'equilateral' | 'isosceles' | 'scalene';
if (triangle != 'scalene')
const isosceles: 'equilateral' | 'isosceles' = triangle;
为更好地激发这个问题,
- 我有一个类型,它几乎是一个可辨别联合(DNS RRs),除了我无法枚举所有辨别的值(通常是一个
字符串 | 数字
,允许扩展)。因此,内置的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)可用。再次感谢!
{kind: string}
不算作区分联合类型,甚至不算作联合类型,因此它不会表现得很好。使用类型保护检查{kind: string}
与另一个{kind: string}
只会使其保持不变(在“true”情况下)或缩小为never
(在“false”情况下),正如您的代码所示。 - jcalz