Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Finding a value in an array by type in TypeScript

Tags:

typescript

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?

like image 482
Chip Bell Avatar asked May 10 '26 00:05

Chip Bell


2 Answers

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

like image 176
jonrsharpe Avatar answered May 11 '26 13:05

jonrsharpe


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);
like image 34
VLAZ Avatar answered May 11 '26 14:05

VLAZ



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!