0
votes

I know that key/value objects in TypeScript are typed like this:

const animals: { [key: string]: string } = { a1: "cat", a2: "dog" };

The above object only allows values of type string.

Now, let's say I have this abstract class

abstract class Animal {
  abstract makeSound(): void;

  move(): void {
    console.log("roaming the earth...");
  }
}

How can I make it so that my animals object can only have actual animals, i.e. classes (not instances) that extends Animal?

I've tried this, but get "cannot find name 'T'" as an error.

const animals: { [key: string]: T extends typeof Animal } = { a1: Cat, a2: Dog };

const animal = new animals["a2"]();

Also tried this, but get "'?' expected"

const animals: { [key: string]: any extends typeof Animal } = { a1: Cat, a2: Dog };

const animal = new animals["a2"]();

If I knew all types of animals beforehand (but I don't) this would work:

const animals: { [key: string]: typeof Cat | typeof Dog } = { a1: Cat, a2: Dog };

const animal = new animals["a2"]();

Is there a way to make this generic?

3
Doesn't { [key: string]: typeof Animal } work for you? Also you could write it nicer as Record<string, typeof Animal>Alex Chashin
@AlexChashin That would work only if you cast e.g. animals["a2"] to any first before trying to create an instance, since otherwise the compiler would complain about Animal being abstract.sloth
So if you're fine with making Animal non-abstract { [key: string]: typeof Animal } should work...sloth

3 Answers

2
votes

Per the comments above, it seems OP may be asking whether one can create a map of classes that all extend Animals (rather than a map of Animal instances). That can be done using a function and generics, like so [Edited to address the comment below]:

abstract class Animal {
  abstract makeSound(): void;

  move(): void {
    console.log("roaming the earth...");
  }
}

class Cat extends Animal {
  makeSound(){console.log("meow")}
}

class Dog extends Animal {
  makeSound(){console.log("woof")}
}

class Tiger extends Animal {
  makeSound(){console.log(“roar”)}
}

function addToAnimals<T extends Record<string, typeof Animal>, U extends Record<string, type of Animal>(animals: T | {}, newAnimal: U){
  return {...animals, ...newAnimal}
}

const animals = addToAnimals({}, {a :Dog, b: Cat})
const animals2 = addToAnimals(animals, {c: Tiger})

const animal = new animals["a"]();
const animal2 = new animals2[“c”]();

Playground here

The basic idea is that the creator function takes in the new extended class with a constraint <T extends Record<string, typeof Animal>> which insures you are only giving it Animals. But TS then infers the actual class value from the parameter and uses it when it builds the new animals object.

1
votes

When writing my comment I didn't notice the abstract thing, so sorry for that.

Anyway, I don't think it's possible to do what you want. Basically your question reduces to "Is there a way to make a type, that includes all children of a given class which are not abstract?", to do this, given a class, you would need to check whether it's abstract or not, which is impossible. So the only option you have, as I see, is to either make Animal not abstract, or just go without types in this place.

Also typeof will not work, if Animal is abstract, because imagine you find a way to make a type ChildrenOf<T>, which includes all children of a class, but not the class itself. If Animal is normal, it's fine, but if Animal is abstract, then you can do this:

abstract class Mammal extends Animal {}

const a: ChildrenOf<Animal> = Mammal
new a()

Here Mammal is assignable to ChildrenOf<Animal>, because it's a child, but it's abstract, so it's not supposed to be instanciated. This problem doesn't appear if Animal is normal, because then Mammal is not assignable to typeof Animal

0
votes

There are a few things going on here. To your main question, when you define an an abstract class (or any class for that matter), you are also defining a type in typescript. So to then restrict a type to that class, e.g., Animal, you simply have to use it's name. So [key:string]: Animal

There are a few other issues here in that you are instantiating Cat and Dog without calling constructors and then instantiating an array with a constructor. Here's what I think is the cleaned up code you want:

abstract class Animal {
  abstract makeSound(): void;

  move(): void {
    console.log("roaming the earth...");
  }
}

class Cat extends Animal {
  makeSound(){console.log("meow")}
}

class Dog extends Animal {
  makeSound(){console.log("woof")}
}

const animals: { [key: string]: Animal } = { a1: new Cat, a2: new Dog };

const animal = animals["a2"];

And a playground here.