首页 > 解决方案 > 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由于条件类型 (),将永远不会比kindso !isTriangle(triangle, kind)will never be 更窄never,但它仍然比应有的更窄(除非K是文字)。

更新 4

再次感谢 @jcalz 和 @KRyan 耐心地解释这实际上是如何实现的,以及随之而来的弱点。我选择了@KRyan 的答案来贡献假名义的想法,尽管您的综合答案非常有帮助!

我的结论是s == kind(or triangle == kindor shape.kind == kind) 的类型是内置的,并且(尚未)可供用户使用,无法分配给其他事物(如谓词)。

我不确定这与单面型防护装置b/c 的错误分支s == kind在(一种)情况下确实变窄的情况完全相同

declare const triangle: 'equilateral' | 'isosceles' | 'scalene';
if (triangle != 'scalene')
  const isosceles: 'equilateral' | 'isosceles' = triangle;

为了更好地激发这个问题

  1. 我有一个几乎是可区分联合(DNS RRs)的类型,除了我无法枚举所有判别式的值(通常它是 a string | number,允许扩展)。因此,内置rr.rdtype == 'RRSIG'行为不适用。除非我首先使用用户定义的类型保护 () 将其缩小为真正的可区分联合,否则isTypedRR(rr) && rr.rdtype == 'RRSIG'这不是一个糟糕的选择。
  2. 我可以为我可以枚举的每个 RR 类型实现用户定义的类型保护,但这是很多重复(function isRRSIG(rr): rr is RR<'RRSIG'>,function isDNSKEY(rr): rr is RR<'DNSKEY'>等)。可能这就是我将继续做的事情:重复但显而易见。
  3. 微不足道的泛型类型守卫的问题在于,非文字不是不允许的,但没有意义(与s == kind/不同rr.rdtype == rdtype)。例如function isRR<T>(rr, rdtype: T): rr is RR<T>。因此这个问题。

这使我无法说换isTypedRR(rr) && rr.rdtype == rdtypefunction isRR(rr, rdtype)。谓词内部rr被合理地缩小,但唯一的选择是(当前)rr is RR<T>(或现在是假名义)。

也许当类型保护被推断出来时,合理地缩小谓词之外的类型也是微不足道的?或者当类型可以被否定时,给定一个不可枚举的判别式,就有可能建立一个真正的可区分联合。我确实希望s == kind用户可以使用(更方便:-P)的类型。再次感谢!

标签: typescript

解决方案


关于缩小条件类型

因此,从根本上讲,您的问题是缩小值不会为了映射或条件类型而缩小其类型。在 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',类型是sis 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技巧 — 将变量转换为更窄类型的版本,然后当pretendis时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需要的pretendnever——确保你的 switch 语句涵盖了pretend's 的缩小类型中的所有可能性。如果您添加了要专门处理的新形状,这将非常有用。

显然,您不必switch在这里使用;一连串的if//将以相同的方式工作else ifelse

关于不完美的字体保护

在您的最后一次迭代中,您的问题是isTriangle返回falsetypeof 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应该使这不可能。

然而,这似乎是一个非常非常糟糕的设计。这可能只是假设的插图场景,但我真的很难确定您为什么要以这种方式设计事物。完全不清楚为什么要检查triangleof 的值kind并将结果显示在类型域中,但没有缩小kind到您实际上可以知道它的类型(因此triangle是 's)的程度。最好先缩小kind,然后用它来缩小triangle——在那种情况下,你没有问题。你似乎在某个地方颠倒了一些逻辑,我认为 Typescript 对此感到很不自在。我当然是。


推荐阅读