Search code examples
typescripttypeguards

Typeguard for generic union type


I have created a union type:

type RequestParameterType = number | string | boolean | Array<number>;

and I have a class which is a key/value pair holding that union type:

class RequestParameter
{
    constructor(name: string, value: RequestParameterType)
    {
        this.Name = name;
        this.Value = value;
    }

    public Name: string;
    public Value: RequestParameterType;
}

I can then make an array of that RequestParameter to hold keys/values:

let parameters: Array<RequestParameter> = new Array<RequestParameter>();
parameters.push(new RequestParameter("one", 1));
parameters.push(new RequestParameter("two", "param2"));

with the idea being that I can write a GetParameter function to return typed values from that array, which in practice I'd probably use like this:

// should return number type, with value 1
let numberParam: number | undefined = this.GetParameter<number>("one", parameters);

// should return string type, with value "param2"
let stringParam: string | undefined = this.GetParameter<string>("two", parameters);

// should return undefined, because param named 'two' is not number type
let undefinedParam: number | undefined = this.GetParameter<number>("two", parameters);

However I'm having an issue with my function to get typed parameters, because I don't know how to check the generic type matches the value type:

function GetParameter<T extends RequestParameterType>(parameterName: string, parameters: Array<RequestParameter>): T | undefined
{
    let result: T | undefined = undefined;

    for (let parameter of parameters)
    {
        // Type check fails: 'T' only refers to a type, but is being used as a value here.
        if (parameter.Name === parameterName && parameter.Value instanceof T )
        {
            // Possibly an issue here too: 
            // Type 'RequestParameterType' is not assignable to type 'T | undefined'.  
            // Type 'string' is not assignable to type 'T | undefined'.
            result = parameter.Value;
        }
    }

    return result;
}

I believe I may need to write a typeguard function, but I'm struggling in the same way to check the generic type when writing the typeguard. Is this going to be possible to resolve?

Here is the example : in the Playground


Solution

  • TypeScript compiles to JavaScript, which is what actually gets run. The type T and its specification like <number> or <string> will be erased upon compilation, so there's nothing called T at runtime to use. The instanceof operator specifically only works when checking against class constructor functions, and since your possible T values are mostly primitives like string and boolean, you wouldn't want to use instanceof anyway ("foo" instanceof String is false).

    Instead, you will probably need to pass a type guard function into GetParameter() as an argument, since such a function will exist at runtime.

    That is, you could change GetParameter() to

    function GetParameter<T extends RequestParameterType>(
      parameterName: string,
      parameters: Array<RequestParameter>,
      guard: (x: RequestParameterType) => x is T // new param
    ): T | undefined {
      let result: T | undefined = undefined;
    
      for (let parameter of parameters) {
        // new check using guard() instead of instanceof
        if (parameter.Name === parameterName && guard(parameter.Value)) {
          result = parameter.Value; // no error
        }
      }
    
      return result;
    }
    

    where guard() has to be a function which can take an object of some RequestParameterType and narrow it to T. Here is a set of those you can use:

    const guards = {
      number: (x: RequestParameterType): x is number => typeof x === "number",
      string: (x: RequestParameterType): x is string => typeof x === "string",
      boolean: (x: RequestParameterType): x is boolean => typeof x === "boolean",
    
      // the only array matching RequestParameterType is number[], so we can
      // just check to see if x is an array  without needing to inspect elements
      numberArray: (x: RequestParameterType): x is number[] => Array.isArray(x) 
    };
    

    And then you can call GetParameter() like this:

    let numberParam = GetParameter("one", parameters, guards.number);
    console.log(numberParam); // 1
    
    let stringParam = GetParameter("two", parameters, guards.string);
    console.log(stringParam); // param2
    
    let undefinedParam = GetParameter("two", parameters, guards.number);
    console.log(undefinedParam); // undefined
    

    Note how guards.number takes the place of <number>. And if you inspect the type of numberParam it is number | undefined, and the returned values are what you expect.

    Okay, hope that helps; good luck!

    Link to code