Hitta ett värde i en array efter typ i TypeScript


Jag försöker bygga en metod för att hitta objekt i en array efter typ. Betrakta denna särskilda uppsättning:

abstract class Animal {}

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

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

Jag försöker skriva en funktion som kan hitta ett element från animals efter dess typ, något i stil med: findAnimal<Dog>(animals). På enkel engelska: "hitta det första elementet i animals som är en instans av Dog." Här är en grov skiss av det:

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

Detta kompilerar inte, och jag förstår varför: det generiska argumentet finns inte längre efter kompilering. Finns det dock ett sätt att få något som detta att fungera? Jag har helt klart inget emot att ändra funktionen så att den tar två parametrar:

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

Men med denna ändring är jag inte säker på hur jag korrekt ska specificera det andra argumentet eller returtypen. Vilka alternativ har jag?

Svar #1

Du kan använda generiken både i typen på klassargumentet och i returvärdet på följande sätt:

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

Observera att genom att använda en typ-predikat som returvärde från find-callbacken behöver vi inte längre någon typförsäkran. Detta är mycket säkrare - du kommer kanske inte att hitta en cls-instans i animals, du returnerar potentiellt undefined som A i din nuvarande implementation.

Den generiska typen kan härledas från det andra argumentet när du anropar detta:

find(animals, Dog);

Lekplats

Svar #2

Istället för att skapa en helt ny funktion kan du skapa ett sökpredikat som också är en typvakt. På så sätt kan du använda den vanliga Array#find-metoden men du säkerställer också att du får tillbaka rätt typ:

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
}

Lekplatslänk

Detta kan sedan generaliseras och göras lätt återanvändbart som en currad funktion:

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
}

Lekplatslänk

Eftersom detta fortfarande är ett predikat kan vi återanvända det för någon av de standardmetoder för arrayer som tar ett.

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

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

Lekplatslänk


Denna funktion kan ytterligare generaliseras för vilket som helst super-/underklassförhållande och för att tillåta valfria variadiska konstruktörer. Var medveten om att dina gränssnitt [inte bör vara tomma[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

Lekplatslänk

Var dock medveten om att den generiska typinferenz bara fungerar så länge den andra funktionen ges en typ. Exemplet ovan behöver inte uttryckligen ange de generiska argumenten eftersom vi anropar med den första parametern Dog vilket sätter Child och sedan passeras den andra funktionen som en array-callback, så den får omedelbart Animal eller Cat som en typ för Super. Men att göra detta:

const searchDog = search(Dog);

anger inte Super-generiska argumentet och det sätts därför till unknown. Om du vill härleda ett predikat och ange typerna, behöver du

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