0

I want to write a typeguard to check if all children of array are of type T thus making it an Array where T is a generic type

TS Playground

// Assume arr of any type but Array
const arr: any[] = [
  {
    foo: "bleh1",
    bar: 1
  },
  {
    foo: "bleh2",
    bar: 2
  },
]

interface newType {
  foo: string
  bar: number
}

// Check that arr is an array of newType , ie arr: newType[]
const isArrayOf = <T,>(arr: any): arr is Array<T> => {
  // TypeScript mastery needed here
  return true
}

if(isArrayOf<newType>(arr)){
  arr
}

Solaris
  • 674
  • 7
  • 22
  • Got suggestion from GitHub Copilot but couldnt understand the 2nd argument ```typescript const isArrayOf = (arr: any[], type: new (...args: any[]) => T): arr is T[] => { return arr.every((item) => item instanceof type) } ``` – Solaris Dec 01 '21 at 20:48
  • 2
    The type system *does not exist at runtime*. You cannot check if something conforms to a generic type because there is no "geneics" nor "types" when the function runs. – VLAZ Dec 01 '21 at 20:48
  • 2
    The suggestion is to use a concrete type and pass in a class. However, that's not possible when the type is an interface. – VLAZ Dec 01 '21 at 20:49
  • I think its possible like described [here] (https://2ality.com/2020/06/type-guards-assertion-functions-typescript.html#example-of-a-user-defined-type-guard%3A-isarrayinstanceof()) but I fail to understand its working thus would like some explanation about the function arguments – Solaris Dec 01 '21 at 20:57
  • Again, this works if you have a *concrete type*. Something that exists at runtime. A class. Not if you have an interface - that only exists at compiletime. – VLAZ Dec 01 '21 at 20:59

2 Answers2

1

The best thing you could do is this:

const arr: NewType[] = [
  {
    foo: "bleh1",
    bar: 1
  },
  {
    foo: "bleh2",
    bar: 2
  },
]

interface NewType {
  foo: string
  bar: number
}

type TypeOfArrayElements<T> = T extends Array<infer U> ? U : never;

type ArrayType = TypeOfArrayElements<typeof arr>;

TypeScript will never be able to guess that the array typed as any[] actually contains NewType elements. Like everything in TypeScript, type predicates are static and won't return a type dynamically based on what is passed as a parameter at runtime. But if you type it as NewType[], then you can extract the NewType type from it.

Guerric P
  • 30,447
  • 6
  • 48
  • 86
0

Edit 1:

This answer has two shortcomings (mentioned in comments)

  • The property needs not be own for it to exist on the object x = {a: 1}; y = Object.create(x); y.b = 2
  • The function requires you to manually enumerate all the keys of the object. Thus can introduce human error

I think the solution can still be used as workaround in specific circumstances

Original:

If an array a is of type newType[] then every element of a , consider x = a[0] will be of type newType. x is of type newType because x satisfies all properties and methods of type newType.

Thus if reversed , if x y z are of type newType and they are the only and all elements of array a, Thus every element of a are of type newType, which satisfies the condition for a to be of type newType[]

// Check if obj has all keys props[]
const hasAllProperties = <T,>(obj: any, props: (keyof T)[]): obj is T => {
  return props.every((prop) => {
    // console.log(prop)
    return Object.prototype.hasOwnProperty.call(obj, prop)})
}

// Check that arr is an array of newType , ie arr: newType[]
const isArrayOf = <T,>(obj: any[], props: (keyof T)[]): obj is T[] => {
  // Check if every elements have all keys in props
  return obj.every((ele) => {
    // console.log(ele)
    return hasAllProperties<T>(ele,props)
  }
  )
}

if (isArrayOf<newType>(arr, ["foo", "bar"])) {
  console.log("arr is of newType[]")
}

TS Playground

Solaris
  • 674
  • 7
  • 22
  • 1
    `Object.prototype.hasOwnProperty.call(obj, prop)})` - the property needs not be own for it to exist on the object. Consider `x = {a: 1}; y = Object.create(x); y.b = 2` now `y` has both fields `a` and `b` thus satisfies `interface AB { a: number; b: number; }` however `a` is not an own property. Consider using `prop in obj` – VLAZ Dec 02 '21 at 13:06
  • 1
    There is also a problem with the function invocation being `isArrayOf(arr, ["foo", "bar"])` - that requires you to manually enumerate all the keys of the object. However, it's not a compilation error if you don't, thus `isArrayOf(arr, ["foo"])` would claim that `arr = [{foo: "hello}, {foo: "world"}]` is `newType[]` when it is not. This could be an issue if the interface changes, for example. Maybe the invocation was correct once but in the future somebody adds or removes a property - they now have to go and change all invocations of `isArrayOf` – VLAZ Dec 02 '21 at 13:09
  • The 2nd drawback can we avoided with https://stackoverflow.com/questions/43909566/get-keys-of-a-typescript-interface-as-array-of-strings – Solaris Dec 02 '21 at 18:26