Search code examples
typescripttypeguards

Any way to propagate type guards in TypeScript?


Suppose I have a bunch of type guard functions and some logic I want to reuse across all of them. Is there any way to propagate the type predicate? I.e. something like:

type TokenType = `NumericLiteral`; // Big union here

interface Token {
  type: TokenType;
  raw: string;
  value?: unknown;
}

interface NumericLiteral extends Token {
  type: `NumericLiteral`;
  value: number;
}

function isNumericLiteral(token: Token): token is NumericLiteral {
  return token.type === `NumericLiteral`;
}

function expectToken(
  token: Token | undefined,
  type: TokenType,
  guardfn: (token: Token) => boolean
) { // Extract the type predicate out of guardfn somehow?
  if (!token || !guardfn(token)) {
    const received = token ? token.type : `nothing`;
    throw new Error(`Expected ${type} but got ${received}`);
  }
}

const token = { type: `NumericLiteral`, value: 123 } as Token;
expectToken(token, `NumericLiteral`, isNumericLiteral); 
// token should now be narrowed as NumericLiteral



Solution

  • An assertion function with a generic type parameter will allow you to infer the type guard from the guard function.

    Note that I didn't only use the generic type on the return type, but also on the type guard parameter (guardFn: (token: Token) => token is T) and the actual type parameter (type: T['type']) to ensure consistency:

    function expectToken<T extends Token>(
      token: Token | undefined,
      type: T['type'],
      guardFn: (token: Token) => token is T
    ): asserts token is T {
      if (!token || !guardFn(token)) {
        const received = token ? token.type : `nothing`;
        throw new Error(`Expected ${type} but got ${received}`);
      }
    }
    

    Complete snippet:

    type TokenType = `NumericLiteral`;
    
    interface Token {
      type: TokenType;
      raw: string;
      value?: unknown;
    }
    
    interface NumericLiteral extends Token {
      type: `NumericLiteral`;
      value: number;
    }
    
    function isNumericLiteral(token: Token): token is NumericLiteral {
      return token.type === `NumericLiteral`;
    }
    
    function expectToken<T extends Token>(
      token: Token | undefined,
      type: T['type'],
      guardFn: (token: Token) => token is T
    ): asserts token is T {
      if (!token || !guardFn(token)) {
        const received = token ? token.type : `nothing`;
        throw new Error(`Expected ${type} but got ${received}`);
      }
    }
    
    const token = { type: `NumericLiteral`, value: 123 } as Token;
    expectToken(token, `NumericLiteral`, isNumericLiteral);
    
    token; // token is narrowed to NumericLiteral
    

    Playground link