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);
find(animals, Dog), its type would betypeof Animal.