ts 缩小 getter

ts narrowing with getter

我实际上正在寻找一种廉价的解决方案,以允许 ts narrowing 使用 getter 或字符串比较而不是 instanceAB instanceof instanceA 来缩小。

ts有解决办法吗?

export class Token {
   type:'TokenContainer'|'TokenPrimitive'|null = null;
   get isContainer(){
      return this.type === 'TokenContainer'
   }
      get isPrimitive(){
       return this.type === 'TokenPrimitive' 
   }
    foo0(){}
}

export class TokenContainer extends Token {
   type:'TokenContainer' = 'TokenContainer';
   foo1(){}
   
}

export class TokenPrimitive extends Token {
    type:'TokenPrimitive' = 'TokenPrimitive'
     foo2(){}
}


let test!:Token;
// is possible to make this work ?.
if(test.isContainer){
   test.foo1()
}
// is possible to make this work ?
if(test.type === 'TokenContainer' ){
   test.foo1()
}

// instead of this
if(test instanceof TokenContainer ){
   test.foo1()
}

link: tsPlayground

我假设您不打算重构代码以更改运行时行为甚至发出的 JavaScript。也就是说,我对您的代码所做的更改只会影响 TypeScript 类型检查器。


属性 TypeScript 中的 getter 在缩小时的行为与常规属性基本相同。 microsoft/TypeScript#43368 to allow property getters to act as user defined type guard functions 上有一个建议,但目前该语言中没有这样的功能。所以如果你想得到你想要的类型检查器行为,我们必须以一种在 isContainerisPrimitive 只是普通属性的情况下也能工作的方式来做。


这意味着您的 isContainer/isPrimitive 检查和 type 检查都需要充当 test 对象类型的类型保护。在 TypeScript 中,属性 检查可以作为包含对象类型的类型保护的唯一方法是对象的类型是否为 discriminated union 并且 属性 是该对象的判别式工会。

Token 类型不是有区别的联合。它甚至根本不是工会;它只是一个 class 实例接口。所以如果 testannotated as Token 那么你想要的是不可能的。那么,启用此功能的第一步是将 test 注释为联合类型:

type SomeToken = TokenContainer | TokenPrimitive;            
declare let test: SomeToken;

我们现在必须确保 SomeToken 是一个可判别联合,其中 isContainerisPrimitivetype 是判别属性。


嗯,type 属性 已经是判别式了。 TokenContainerTokenPrimitive 都有一个 type 属性,其类型是不同的字符串 literal type。这样检查就会立即生效:

if (test.type === 'TokenContainer') {
   test.foo1() // okay
} else {
   test.foo2() // okay
}

isContainerisPrimitive 的情况不同。它们仅在 Token superclass 上声明,并由 subclass 继承。如果您在适当的硬编码布尔文字类型的 subclasses 上显式声明它们,这将开始工作。但是我们不想通过显式覆盖它们来进行任何运行时更改,因此您需要执行类似 the declare property modifier in the subclasses, which unfortunately are not legal when the parent property is a getter; see microsoft/TypeScript#40220, so the current workaround is to use (yuck) a //@ts-ignore comment:

的操作
export class TokenContainer extends Token {
   type: 'TokenContainer' = 'TokenContainer';
   //@ts-ignore
   declare readonly isContainer: true;
   //@ts-ignore
   declare readonly isPrimitive: false;
   foo1() { }
}

export class TokenPrimitive extends Token {
   type: 'TokenPrimitive' = 'TokenPrimitive'
   //@ts-ignore
   declare readonly isContainer: false;
   //@ts-ignore
   declare readonly isPrimitive: true;
   foo2() { }
}

您可以验证这是否有效:

if (test.isContainer) {
   test.foo1() // okay
} else {
   test.foo2() // okay
}

但在 microsoft/TypeScript#40220 实施之前我不推荐这样做。


相反,您可以使父 class 属性 类型有条件,具体取决于实现子 class 的类型,由 the polymorphic this type. That is, you leave TokenContainer and TokenPrimitive alone, but assert 表示,吸气剂 return 取决于实际的子class类型:

export class Token {
   type: 'TokenContainer' | 'TokenPrimitive' | null = null;
   get isContainer() {
      return (this.type === 'TokenContainer') as 
        this extends TokenContainer ? true : false
   }
   get isPrimitive() {
      return (this.type === 'TokenPrimitive') as 
        this extends TokenPrimitive ? true : false
   }
   foo0() { }
}

this extends TokenContainer ? true : false这样的类型在class实现中或多或少是未解决的generic类型,但是当你使用子classes的实例时,它们会解析为 truefalse。然后这也将起作用:

if (test.isContainer) {
   test.foo1() // okay
} else {
   test.foo2() // okay
}

Playground link to code