Reputation: 75
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?
Upvotes: 1
Views: 162
Reputation: 19070
You can do:
enum EAnimalType {
bird = 'bird',
dog = 'dog',
}
interface IAnimal {
name: string;
}
interface IBird {
information: IAnimal;
type: EAnimalType.bird;
canFly: boolean;
}
interface IDog {
information: IAnimal;
type: EAnimalType.dog;
tailLength: number;
}
class Animal implements IAnimal {
name!: string;
}
class Bird extends Animal implements IBird {
information!: Animal;
type: EAnimalType.bird = EAnimalType.bird;
canFly!: boolean;
}
class Dog extends Animal implements IDog {
information!: Animal;
type: EAnimalType.dog = EAnimalType.dog;
tailLength!: number;
}
type AnimalDTO = IBird | IDog;
function petDog(dog: IDog) {
console.log(`Petting ${dog.information.name} with tail length ${dog.tailLength}`);
}
function registerAnimal(animalDto: AnimalDTO) {
if (animalDto.type == EAnimalType.dog) {
petDog(animalDto);
}
}
registerAnimal({
type: EAnimalType.dog,
information: {
name: 'Yaki'
},
tailLength: 3,
});
Check the working code at: www.typescriptlang.org/play
Upvotes: 0
Reputation: 1074138
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.
Upvotes: 4
Reputation: 23825
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);
}
}
Upvotes: 0