Is it possible to have superclasses/interfaces and subclasses/implementations that depict the typing in an abstract way?
Lets say you have types and following functions:
class Animal {
name: string;
}
class Bird extends Animal {
canFly: boolean
}
class Dog extends Animal {
tailLength: number;
}
enum animals {
bird = 'bird',
dog = 'dog'
}
class AnimalDTO {
type: animals;
information: Animal;
}
function registerAnimal(animalDto: AnimalDTO) {
if (animalDto.type == animals.dog) {
petDog(animalDto.information);
}
}
function petDog(dog: Dog) {
console.log(`Petting ${dog.name} with tail length ${dog.tailLength}`)
}
Now in the registerAnimal function it complains
Argument of type 'Animal' is not assignable to parameter of type 'Dog'.
Property 'tailLength' is missing in type 'Animal' but required in type 'Dog'.
How can I have a function that accepts this global DTO and I can apply logic to, based on the passed type, project the passed information to the specific subclass?
CodePudding user response:
The problem is that you're trying to narrow the type of animalDto.information
based on animalDto.type
, which will only work if animalDto
is a union of DTOs for specific animals (more on that below).
The usual way to do this is to not have an Animal
base class, and instead have a union type of specific animal types (this is called a discriminated union — a union where you can tell the members apart by some characteristic [name
in this case]):
interface Bird {
name: "bird";
canFly: boolean;
}
interface Dog {
name: "dog";
tailLength: number;
}
type Animal = Bird | Dog;
(I've used interfaces there, but you could do it with Bird
and Dog
classes as well.)
That way, we can differentiate animals based on the type of their name
(which is a string literal type, "bird"
or "dog"
, not just a string):
interface AnimalDTO {
information: Animal;
}
function registerAnimal(animalDto: AnimalDTO) {
const animal = animalDto.information;
if (animal.name == "dog") {
petDog(animal);
}
}
function petDog(dog: Dog) {
console.log(`Petting ${dog.name} with tail length ${dog.tailLength}`);
}
If AnimalDTO
has to have a separate type
, you can do the same thing with it that I did with Bird
and Dog
above:
interface BirdDTO {
type: "bird";
information: Extract<Animal, {name: "bird"}>;
}
interface DogDTO {
type: "dog";
information: Extract<Animal, {name: "dog"}>;
}
type AnimalDTO = BirdDTO | DogDTO;
function registerAnimal(animalDto: AnimalDTO) {
if (animalDto.type == "dog") {
petDog(animalDto.information);
}
}
The Extract
bit identifies the specific name with a name
matching the type
we're using for the DTO.
If there are a lot of animals, repeating "bird"
and "dog"
in both places is error-prone, so we can use a generic type to create them:
type MakeAnimalDTO<AnimalType extends string> = {
type: AnimalType;
information: Extract<Animal, {name: AnimalType}>;
}
type BirdDTO = MakeAnimalDTO<"bird">;
type DogDTO = MakeAnimalDTO<"dog">;
type AnimalDTO = BirdDTO | DogDTO;
There are lots of ways to do this, but the fundamental thing is to have a type-based way of differentiating the types from one another; in the above that's the "bird"
and "dog"
string literal types.
CodePudding user response:
It is kind of awkward to tell TypeScript that there is a relationship between the type
and information
properties. But you could define all possible relations in a separate type:
class AnimalDTO {
type!: animals
information!: Animal
}
type AnimalDTOtype = {
type: animals.bird,
information: Bird
} | {
type: animals.dog
information: Dog
}
function registerAnimal<T extends keyof typeof animals>(animalDto: AnimalDTOtype) {
if (animalDto.type == animals.dog) {
petDog(animalDto.information);
}
}
CodePudding user response:
You can do:
class Animal {
name!: string;
}
class Bird extends Animal {
canFly!: boolean;
}
class Dog extends Animal {
tailLength!: number;
}
enum EAnimalType {
bird = 'bird',
dog = 'dog',
}
class AnimalDTO {
type!: EAnimalType;
information!: Animal;
tailLength?: number;
}
function petDog(dog: Dog) {
console.log(`Petting ${dog.name} with tail length ${dog.tailLength}`);
}
function registerAnimal(animalDto: AnimalDTO) {
if (animalDto.type == EAnimalType.dog) {
const dog = new Dog();
dog.name = animalDto.information.name;
dog.tailLength = animalDto.tailLength || 0;
petDog(dog);
}
}
registerAnimal({
type: EAnimalType.dog,
information: {
name: 'Yaki',
},
tailLength: 3,
});
Check the working code at: www.typescriptlang.org/play