arraystypescriptinstanceof

How to check instance of the generic in Array with Typescript


I have a function that receives an array, but those can be of different type. Depending on the type I need to format differently:

public format(value: Foo[] | Bar[]) {
  // this does not work
  if (value instanceof Foo[]) ...
}

I know I can use instanceof to check if I have an object of a certain class in Typescript.

new Foo() instanceof Foo // true

This also works for checking if I have an Array.

new Array<Foo> instanceof Array // true

But I cannot check if my Array actually is typed to Foo

new Array<Foo>() instanceof Array<Foo> 
// The right-hand side of an 'instanceof' expression must be of type 'any' 
// or of a type assignable to the 'Function' interface type.

Is there any way to explicitly check for the type of values of an Array?


Solution

  • At runtime there's no difference between Array<Foo> and Array<Bar>; the static type system is erased from the emitted JavaScript, so all you have is some JavaScript array. So you'll need to write your own test that operates at runtime and then tell the compiler what you're doing so you get the benefits of static typing.


    One way would be to write some suitable user-defined type guard functions, which would let you do this:

    public format(value: Foo[] | Bar[]) {
        const isFooArray = isArrayOf(isInstanceOf(Foo));
    
        if (isFooArray(value)) {
            // true block
            for (const foo of value) {
                const f: Foo = foo; // okay
            }
        } else {
            // false block
            for (const bar of value) {
                const b: Bar = bar; // okay
            }
        }
    }
    

    The compiler understands that inside the true block, value has been narrowed from Foo[] | Bar[] to just Foo[], and that inside the false block, value has been narrowed from Foo[] | Bar[] to just Bar[]. This has to do with the type signature for isFooArray(), a type guard created by composing the output of two other functions, isArrayOf() and isInstanceOf().

    Let's examine their definitions:

    const isArrayOf = <T>(elemGuard: (x: any) => x is T) =>
        (arr: any[]): arr is Array<T> => arr.every(elemGuard);
    

    The function isArrayOf() takes a type guard function elemGuard for a single array element and returns a new type guard that calls elemGuard on every element of an array. If all the elements pass the test, then you have an array of the guarded type. If even a single element does not pass, then you don't. You could, if you want, check just one element, but you run the risk of accidentally treating a heterogeneous array like Array<Foo | Bar> as a Foo[]. Also note that this implies an empty array [] will always pass the test; so an empty array will be considered both a Foo[] and a Bar[].

    const isInstanceOf = <T>(ctor: new (...args: any) => T) =>
        (x: any): x is T => x instanceof ctor;
    

    The isInstanceOf() function just wraps the normal instanceof test into a user-defined type guard suitable to use with isArrayOf().

    So const isFooArray = isArrayOf(isInstanceOf(Foo)) is a composite type guard that specifically checks to see if the array it is examining is a Foo[], by examining each element and doing an instanceof check.


    Playground link to code