Search code examples
typescripttypescript-genericstypeguards

User defined type guards for array element type


I am trying to code a user defined type guard that check the types of the element of an array.

This is what I have:

const isArray = <T>(o: any | any[]): o is T[] => {
  return Array.isArray(o)
}

So I can call it this way

const func = function(arg: string | string[] | number | number[]) {
  if (isArray<string>(arg)) {
    console.log(arg)
  } else {
    console.log(arg)
  }
}

This check that arg is an array of string.

I can also use it this way:

const func = function(arg: string | string[] | number | number[]) {
  if (isArray(arg)) {
    console.log(arg)
  } else {
    console.log(arg)
  }
}

And now, it's checking is an array (string[] or number[] here). But the problem it's that the type guard can be called with random generic type, for example Boolean resulting in a weird behavior.

So I went for this function:

const isArray = <T extends U, U>(o: U | U[]): o is T[] => {
  return typeof o === 'object' && typeof o['length'] === 'number'
}

So now, there is a constraint on the type guard. If I call it this way:

isArray<string, typeof arg>(arg)

I will have an error if the first generic type is not one of the arg types. This leads to a new issue.

const func = function(arg: string[] | number[]) {
  if (isArray<string, typeof arg>(arg)) {
    console.log(arg)
  } else {
    console.log(arg)
  }
}

In this case, I will have an error because string is not part of arg types.

So I have to issues:

  • Is there a way to fix the second generic (typeof arg) ? It would be great that it is hardcoded and that user doesn't have to provide it every time he uses the type guard
  • Is there a way to say, "this generic must be of the type of the element of the arrays passed as argument". As example string | number if isArray is called with a parameter of type number[] | string[]

Solution

  • What you are trying to do makes little sense to me. Seems like your type guards are lying - they assert blindly that if something is an Array it's an Array of a given type.

    Your type guard should check if the array members are indeed of the given type, and since you can't know the given type at runtime (which is when the type guard is running), you can't do it in a generic way.

    For example, isStringArray can be written like this:

    const isStringArray = (arg: any): arg is string[] => {
       return Array.isArray(arg) && arg.every(item => typeof item === 'string')
    }
    

    The check must be specific to the type you want to assert.