2

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
}

5
  • Got suggestion from GitHub Copilot but couldnt understand the 2nd argument typescript const isArrayOf = <T>(arr: any[], type: new (...args: any[]) => T): arr is T[] => { return arr.every((item) => item instanceof type) } Commented Dec 1, 2021 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. Commented Dec 1, 2021 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. Commented Dec 1, 2021 at 20:49
  • I think its possible like described [here] (2ality.com/2020/06/…) but I fail to understand its working thus would like some explanation about the function arguments Commented Dec 1, 2021 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. Commented Dec 1, 2021 at 20:59

2 Answers 2

4

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.

Sign up to request clarification or add additional context in comments.

Comments

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

3 Comments

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
There is also a problem with the function invocation being isArrayOf<newType>(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<newType>(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
The 2nd drawback can we avoided with stackoverflow.com/questions/43909566/…

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.