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; }'.

我尝试编写一个泛型类型谓词来解决这个问题,但以下不起作用,因为 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 == kindshape.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 RRs)的类型,除了我无法枚举所有判别式的值(通常它是 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 == rdtypefunction isRR(rr, rdtype) .谓词内 rr被合理缩小,但唯一的选择是(当前)rr is RR<T> (或者现在是假名)。

    也许当type guards are inferred ,合理地缩小谓词之外的类型也很简单吗?或者当types can be negated ,给定一个不可枚举的判别式,就有可能建立一个真正的判别联合。我希望 s == kind 的类型(更方便:-P)可供用户使用。再次感谢!

    最佳答案

    关于缩小条件类型
    所以从根本上说,你的问题是缩小一个值并不会为了映射或条件类型而缩小它的类型。见 this issue on the GitHub bug tracker ,特别是 this comment解释为什么这不起作用:

    If I've read correctly, I think this is working as intended; in the general case, the type of foobar itself doesn't necessarily reflect that FooBar (the type variable) will describe identical types of a given instantiation. For example:

    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' 之前或 s === 'square' , s 的类型是 string , 所以你得到的推论 isShapeOfKind<string>(shape, s)这只告诉我们shape is Shape<string>我们已经知道了(错误的情况是 never 因为 shape 被定义为 Shape ,即 Shape<string> ——它永远不会不是)。你希望发生的事情(但 Typescript 不做的事情)是让它变成类似 Shape<typeof s> 的东西。然后作为关于 s 的更多信息下定决心,了解shape决心,决意,决定。 Typescript 不会跟踪可能相互关联的单独变量的类型。
    你可以这样做的另一种方法是让事物不是一个单独的变量,如果你真的必须这样做的话。也就是说,定义几个接口(interface),如
    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技巧——将变量转换为更窄的类型,然后当 pretendnever ,你知道原来的变量实际上不是那个更窄类型的一部分。此外,较窄的类型必须是 ShapeMatchesKind<A> | ShapeMatchesKind<B> | ShapeMatchesKind<C>而不是 ShapeMatchesKind<A | B | C>因为一个 ShapeMatchesKind<A | B | C>可能有 shape: Shape<A>kind: C . (如果你有一个联合 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 中的所有可能性的窄型。如果您添加了要专门处理的新形状,这将非常有用。
    您不必使用 switch显然,这里; if 的链子/else if/else将以同样的方式工作。
    关于不完美的字体保护
    在您的最后一次迭代中,您的问题是 isTriangle返回 falsetypeof triangle & typeof kind什么时候真的是falsetriangle 的值吗?和 kind 的值不符合。因此,您会遇到 Typescript 同时看到 'equilateral' 的情况。和 'isosceles'排除,因为typeof kind'equilateral' | 'isosceles'但是 kind的实际值(value)只是这两件事之一。
    您可以通过 fake nominal types 解决此问题,所以你可以做类似的事情
    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这里— switch由于某种原因似乎不起作用,它允许 case 'scalene'在第二 block 没有投诉,即使 triangle 的类型那时应该使这不可能。
    然而 ,这似乎是一个非常非常糟糕的设计。这可能只是假设的插图场景,但我真的很难确定您为什么要以这种方式设计事物。完全不清楚您为什么要检查 trianglekind 的值相反并使结果出现在类型域中,但不缩小 kind以至于您实际上可以知道它的类型(因此是 triangle 的)。最好缩小kind首先,然后用它来缩小 triangle ——在那种情况下,你没有问题。你似乎在某个地方颠倒了一些逻辑,我认为 Typescript 对此感到很不自在。我当然是。

    关于TypeScript 泛型类型谓词,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57750277/

    相关文章:

    node.js - 每次我尝试部署功能时,我都会收到 "Cannot find type definition file for ..."

    angular - 在 Promise 链中使用 `.setTimeout()` 的正确方法是什么?

    Angular2 指令 - 字符串问题

    angular - 服务的生命周期 Hook

    jquery - Typescript 运行时类型测试

    angularjs - 我如何在 typescript 构造函数之外正确使用 Angular $timeout 服务?

    javascript - Angular 2 : is styleUrls relative to the current component?

    javascript - 在 JavaScript 中定义一个类?

    javascript - grunt-typescript 不会在指定的 `dest` 中生成 js 文件

    node.js - 从 TypeScript 项目中的一个文件中导出所有接口(interface)/类型