Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Is there in TypeScript a way to use "extends T" as a type?

Problem

I want to define a person with a pet that is something that extends Animal, and is not just an Animal:

interface Person {
  name: string
  pet: extends Animal  //I'm looking for something that would work here
}

What I tried

So let’s say we have these interfaces:

interface Animal {
  name: string
}

interface Fish extends Animal {
  swim: () => void
}

interface Bird extends Animal {
  fly: () => void
}
1. use the parent type

I know I can do:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

interface Person {
  name: string
  pet: Animal // Not what I want, because it needs to be only things that extend Animal
}

But that would hinder inference. Because if pet is not a Fish, it should be a Bird. But this way it can just as well be a plain Animal.

2. union type

To only use types that extends Animal, I can pick them myself with a union type:

interface Person {
  name: string
  pet: Fish | Bird // Not what I want, because I would need to update it with new animals
}

But if I want to create more animal types, I would have to update Person every time.

For example, if I add:

interface Mole extends Animal {
  dig: () => void
}

I want Person to automatically accept Mole as a pet without explicitly adding it by changing pet: Fish | Bird to pet: Fish | Bird | Mole.

Summary

I’m looking for a way that accepts anything that extends Animal, but not the parent Animal itself, without having to update it every time a new animal comes along.
How can I do this in Typescript?

Note

I do not need Animal to exist as an interface, but it would be nice if it can stay an interface.

>Solution :

This is where generics come in to the picture:

interface Person<T extends Animal> {
  name: string
  pet: T
}

let personWithBirdPet: Person<Bird>;
let personWithFishPet: Person<Fish>;
let personWithMolePet: Person<Mole>;

Generics are kind of like "type arguments", much in the same way as you can define "value arguments" for functions (function(foo, bar) {}) we can define "type arguments" called generics for most type structures (including function function<A, B>(foo: A, bar: B) {}, and interfaces as you can see above).

When using generics we can apply constraints to the generics, by using the extends keyword. For example function<A extends number>(value: A) {} would be pretty much the same as writing function(value: number) {}. The version using generics is more powerful, but also more verbose. In this simple function example using generics would be overkill. However in your interface case, using generics would be exactly what you need.

Bear in mind the duck-typing

Since typescript is duck-typed (if it walks like a duck and talks like a duck, then it is a duck). You can get some unexpected behavior that you need to be aware of.

For instance, you can have a person with a generic animal as a pet:

let personWithGenericPet: Person<Animal>;

..or you can have a person with a pet that doesn’t "extend" animal, but looks like an animal:

let personWithStrangePet: Person<{ name: 'bob' }>;

..and speaking of which, a Person looks like and animal, so you can have a person with a person as a pet:

let personWithPersonPet: Person<Person<Animal>>;

These are not issues caused by generics, but are issues that are core to how typescript works.

playground

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading