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

时间:2020-09-04 18:54:29

标签: typescript intellisense typescript-typings typeguards

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

使用打字稿处理策略模式。这是代码

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'})

然后;

同时使用strategy获取sm.getStrategy。我需要类型为name的{​​{1}}参数

因此,Intellisense将引发如下错误:

'foo' | 'bar'

2 个答案:

答案 0 :(得分:1)

无法按照说明进行操作。

Typescript不能跟踪您添加的所有可能的内容,因为它实际上不能真正运行您的代码。如果在运行时可以使用任何字符串值,则打字稿可能无法帮助您限制该类型。

如果有的话怎么办?

sm.addStrategy(await getAsyncStrategyFromServer())

类型系统无法知道将要使用的名称,因为在编译时无法知道它。编译器无法帮助您完成编译不知道的事情。


您必须考虑什么是编译时类型错误以及什么是运行时错误。

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

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

  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`)
    }
  }

答案 1 :(得分:1)

受@ChisBode的评论启发,如果按如下所示更改实现,就可以实现。

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

这是一个可行的原型:

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调用都会返回一个新对象,该对象的类型需要进一步完善。

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

  3. 由于我们遵循的是不变的设计模式,因此我们应该真正确保不能通过其他方式修改管理器基础的策略数组。为此,我们可以从一个类过渡到一个工厂,通过闭包获得严格的隐私权,并减少必须写的代码量:

    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实现封装,但是闭包在更多环境中受支持,并且简单且经过了实战测试。