Search code examples
typescriptintellisensetypescript-typingstypeguards

How to declare type guard dynamically in typescript?


I want to set type guard dynamically depends on array elements

Working on strategy pattern in typescript. here is the code

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

Suppose strategies are added like so:

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

Then;

while getting strategy using sm.getStrategy. I need name parameter of type 'foo' | 'bar'

Thus intellisense will throw error like so:

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

Solution

  • Inspired by @ChisBode's comment, you can achieve this if you alter your implementation as follows.

    Instead of using a mutable object that builds up an array value via successive mutations, you can design your strategy manager as an immutable object that builds up an array type via successive transformations.

    Here's a working prototype:

    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

    Notes:

    1. Each withStrategy call returns a new object with a type that is further refined.

    2. The constraint doesn't need to involve the Strategy type, it could be an arbitrary string.

    3. Since we are following an immutable design pattern, we should really ensure that the strategies array underlying the manager cannot be modified via other means. To accomplish this, we can transition from a class to a factory, obtaining hard privacy via closures and reducing the amount of code we have to write as a bonus:

      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. You could also achieve encapsulation via the Stage 3 ECMAScript private fields proposal but closures are supported in more environments and are simple and battle tested.