如何在打字稿中动态声明类型保护?

How to declare type guard dynamically in typescript?

我想根据数组元素动态设置类型保护

正在研究打字稿中的策略模式。这是代码

class StrategyManager {
  strategies: Strategy[]
  constructor() {
    this.strategies = []
  }

  addStrategy(strategy: Strategy) {
    this.strategies.push(strategy)
  }

  getStrategy(name: <dynamic>) { // this is where I need dynamic type guard
    return this.strategies.find((strategy) => strategy.name === name)
  }
  
}

假设策略是这样添加的:

const sm = new StrategyManager()
sm.addStrategy({name:'foo'})
sm.addStrategy({name:'bar'})

然后;

同时使用 sm.getStrategy 获得 strategy。我需要 name 类型的参数 'foo' | 'bar'

因此 intellisense 会抛出如下错误:

sm.getStrategy('baz') // Intellisense error: `Argument of type '"baz"' is not assignable to parameter of type '"foo" | "bar"'`

无法按照描述完成。

Typescript 无法跟踪您添加的所有可能内容,因为它并不是真正的 运行 您的代码。如果在 运行 时可以使用任何字符串值,那么 typescript 可能无法帮助您约束该类型。

如果你有呢?

sm.addStrategy(await getAsyncStrategyFromServer())

类型系统无法知道它会有什么名字,因为它在编译时无法知道。编译器无法帮助您处理编译器不知道的事情。


你得想想什么是编译时类型错误,什么是运行时间错误

在这种情况下,在编译时,string 是任何有策略名称的地方的正确类型。这是因为,如您所说,名称可以是任何字符串。

但是如果你得到一个没有被添加的策略名称是运行时间错误,因为策略是动态添加的在运行时间。这意味着您使用逻辑而不是类型系统来处理该错误。

  getStrategy(name: string) {
    const strategy = this.strategies.find((strategy) => strategy.name === name)
    if (strategy) {
      return strategy
    } else {
      // or something.
      throw new Error(`Strategy with name: ${name} not found`)
    }
  }

受@ChisBode 评论的启发,如果您按如下方式更改实现,则可以实现此目的。

不是使用 mutable 对象通过连续的突变构建数组 value,您可以将策略管理器设计为 不可变 对象,通过连续转换构建数组类型

这是一个工作原型:

class StrategyManager<N extends Strategy['name'] = never> {
  strategies: (Strategy & { name: N })[] = [];

  withStrategy<S extends Strategy>(strategy: S): StrategyManager<N | S['name']> {
    const result = new StrategyManager<N | S['name']>();
    result.strategies = [...this.strategies, strategy];
    return result;
  }

  getStrategy<T extends N>(name: T) {
    return this.strategies.find(
      (strategy): strategy is typeof strategy & { name: T } => strategy.name === name
    );
  }
}


new StrategyManager()
  .withStrategy({ name: 'bar' })
  .getStrategy('foo')?.name // error as desired

new StrategyManager()
  .withStrategy({ name: 'bar' })
  .getStrategy('bar')?.name // ok; typeof name is 'bar' | undefined


new StrategyManager()
  .withStrategy({ name: 'bar' })
  .withStrategy({ name: 'foo' })
  .getStrategy('foo')?.name // ok; typeof name is 'foo' | undefined

type Strategy = { name: 'foo' | 'bar' };

Playground Link

备注:

  1. 每个 withStrategy 调用 returns 一个具有进一步细化类型的新对象。

  2. 约束不需要涉及Strategy类型,可以是任意string.

  3. 由于我们遵循的是不可变设计模式,因此我们确实应该确保不能通过其他方式修改管理器下的策略数组。为此,我们可以从 class 过渡到工厂,通过闭包获得硬隐私并减少我们必须编写的代码量作为奖励:

    function strategyManager<N extends Strategy['name'] = never>(
      strategies: (Strategy & { name: N })[] = []
    ) {
      return {
        withStrategy<S extends Strategy>(strategy: S) {
          return strategyManager<N | S['name']>([...strategies, strategy]);
        },
        getStrategy<T extends N>(name: T) {
          return strategies.find(
            (strategy): strategy is typeof strategy & { name: T } => strategy.name === name
          );
        }
      };
    }
    
    strategyManager()
      .withStrategy({ name: 'bar' })  
      .getStrategy('foo')?.name // error as desired
    
    strategyManager()
      .withStrategy({ name: 'bar' })  
      .getStrategy('bar')?.name // ok; typeof name is 'bar' | undefined
    
    
    strategyManager()
      .withStrategy({ name: 'bar' })
      .withStrategy({ name: 'foo' })
      .getStrategy('foo')?.name // ok; typeof name is 'foo' | undefined
    
    type Strategy = { name: 'foo' | 'bar' };
    

    Playground Link

  4. 您也可以通过 Stage 3 ECMAScript private fields proposal 实现封装,但是在更多环境中支持闭包,并且简单且经过实战检验。