2

I'm trying to build a method for finding items in an array by type. Consider this particular setup:

abstract class Animal {}

class Dog extends Animal {}
class Cat extends Animal {}
class Horse extends Animal {}

const animals: Animal[] = [new Dog, new Cat, new Horse];

I'm trying to write a function that can find an element from animals by its type, something like: findAnimal<Dog>(animals). In plain English: "find the first element of animals that is an instance of Dog." Here's a rough sketch of that:

function find<A extends Animal>(items: Animal[]): A {
    return items.find((a: Animal) => a instanceof A) as A;
}

This doesn't compile, and I understand why: that generic argument no longer exists after compilation. However, is there a way to get something like this to work? I'm certainly okay with modifying the function to take two parameters:

function find(items: Animal[], A: NOT_SURE_WHAT_GOES_HERE): WHAT_TO_RETURN {
  // ...
}

However, with this change I'm not sure how to properly specify that second argument or return type. What are my options?

2
  • You'd have to pass the class as a parameter, not a generic; find(animals, Dog), its type would be typeof Animal. Commented Nov 15, 2020 at 22:18
  • But how would you specify the return type? Commented Nov 15, 2020 at 22:19

2 Answers 2

5

You can use the generic in both the type of the class argument and the return value as follows:

function find<A extends Animal>(animals: Animal[], cls: new() => A): A | undefined {
    return animals.find((value): value is A => value instanceof cls);
}

Note that, by using a type predicate as the return value from the find callback, we don't need a type assertion any more. This is much safer - you might not find a cls instance in animals, you're potentially returning undefined as A in your current implementation.

The generic can be inferred from the second argument when you call this:

find(animals, Dog);

Playground

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

2 Comments

This did the trick! However, for future readers: If Dog takes constructor arguments you'll need to clarify a tad: cls: new(...args: any[]) => A to get that to work. Otherwise the compiler will fail with something like the cryptic message here: "typeof Dog is not assignable to new() => Dog"
@ChipBell yes, the construct signature has to match whatever is actually being constructed, you basically rearrange the call new Thing(arg1, arg2) to the signature new(arg1: Type1, arg2: Type2) => Thing.
3

Instead of making an entirely new function, you can create a search predicate hat is also a type guard. This way you can call the vanilla Array#find method but you also ensure you get the correct type back:

abstract class Animal {}

class Dog extends Animal { bark() {} }
class Cat extends Animal { meow() {} }
class Horse extends Animal { neigh() {}}

const animals: Animal[] = [new Dog, new Cat, new Horse];

const animal = animals.find((item: Animal): item is Dog => item instanceof Dog);

if (animal) { //make sure it's not `undefined`
  animal.meow();  //Error - it's not a Cat
  animal.neigh(); //Error - it's not a Horse
  animal.bark();  //OK - it's a Dog
}

Playground Link

This can then be generalised and made easily reusable as a curried function:

abstract class Animal {}

class Dog extends Animal { bark() {} }
class Cat extends Animal { meow() {} }
class Horse extends Animal { neigh() {}}

const animals: Animal[] = [new Dog, new Cat, new Horse];

const search = <A extends Animal>(species: new() => A) => (item: Animal): item is A =>
  item instanceof species;

const animal = animals.find(search(Dog));

if (animal) { //make sure it's not `undefined`
  animal.meow();  //Error - it's not a Cat
  animal.neigh(); //Error - it's not a Horse
  animal.bark();  //OK - it's a Dog
}

Playground Link

Since this is still a predicate, we can re-use it for any of the default array methods that take one

const dogs = animals.filter(search(Dog)); //=> Dog[]
dogs.forEach(dog => dog.bark()); //OK

if (animals.every(search(Dog))) {
  animals.forEach(dog => dog.bark()); //OK
}

Playground Link


This function can be further generalised for any super/subclass relationship and to allow any variadic constructors. Do be aware that your interfaces [should not be empty[https://github.com/microsoft/TypeScript/wiki/FAQ#why-are-all-types-assignable-to-empty-interfaces):

abstract class Animal { isPet?: boolean }

class Dog extends Animal { bark() {} }
class Cat extends Animal { constructor(name: string) {super(); } meow() {} }
class Horse extends Animal { constructor(age: number, recehorse: boolean) {super();} neigh() {}}

declare const animals: Animal[];

const search = <Super, Child extends Super>(specific: new(...args: any[]) => Child) => (item: Super): item is Child =>
  item instanceof specific;

const dog = animals.find(search(Dog));     //OK
const cat = animals.find(search(Cat));     //OK
const horse = animals.find(search(Horse)); //OK

const dogs = animals.filter(search(Dog));  //OK

if (animals.every(search(Dog))) { }        //OK

animals.find(search(String)); // Error - String is not assignable to Animal

declare const cats: Cat[];
cats.find(search(Dog)); //Error - Dog is not a assignable to Cat

Playground Link

However, be aware that the generic type inference only works as long as the second function is given a type. The example above doesn't need to explicitly supply the generic arguments because we call with the first parameter Dog which sets Child and then the second function is passed as an array callback, so it immediately gets Animal or Cat as a type for Super. However, doing this:

const searchDog = search(Dog);

does not set the Super generic argument and thus it gets set to unknown. If you want to derive a predicate and set the types, you need

const searchDog = search<Animal, Dog>(Dog);

Comments

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.