DEV Community

Cover image for Three Ways of Using "extends" in TypeScript
Tomohiro Yoshida
Tomohiro Yoshida

Posted on • Edited on

Three Ways of Using "extends" in TypeScript

When writing a TypeScript code, you will often see the "extends" keyword. But please be careful! This keyword works differently according to its context.
In this article, I am going to explain how to use "extends" in three different ways in TypeScript.

1. Inheritance

The "extends keyword" will be used for Interface Inheritance (or Class Inheritance). Here is an example of the Interface Inheritance:

interface Vehicle {
    wheels: number;
    maker?: string;
}

interface Car extends Vehicle {
    power: "gas" | "electricity";
}

interface Bicycle extends Vehicle {
    folding: boolean;
}

const myCar: Car = {
    wheels: 4,
    maker: "Toyota",
    power: "gas"
} // OK

const car: Car = {
    wheels: 4,
    maker: "Honda",
    power: "gas",
    folding: false
} // Error
// Type '{ wheels: number; maker: string; power: "gas"; folding: boolean; }' is not assignable to type 'Car'.
    // Object literal may only specify known properties, and 'folding' does not exist in type 'Car'.

const bicycle: Bicycle = {
    wheels: 2,
} // Error
// Property 'folding' is missing in type '{ wheels: number; }' but required in type 'Bicycle'.
Enter fullscreen mode Exit fullscreen mode

You can make an inherited interface from a base interface. (In this example, Vehicle is the base interface, and Car and Bicycle are the inherited interfaces.)
If you want to make a new interface taking over properties from other interfaces, this technique is useful because you do not have to declare a new interface from scratch again.
(I discussed Interface Extensions and Merging in detail in another article, so please read this one if you want to know more about it.)

2. Generic Constraints

If you are an intermediate or advanced TypeScript programmer, you may want to use Generic Types in your code. For example, if you want to get the value from an object's property, you may write the code below:

const testObj = { x: 10, y: "Hello", z: true };

function getProperty<T>(obj: T, key: keyof T) {
  return obj[key];
}

const xValue = getProperty(testObj, 'x');
const yValue = getProperty(testObj, 'y');
Enter fullscreen mode Exit fullscreen mode

The above code is functional. But this is not type safe enough. Let me hover my cursor over xValue and yValue and see their types.

const xValue = getProperty(testObj, 'x');
// const xValue: string | number | boolean

const yValue = getProperty(testObj, 'y');
// const yValue: string | number | boolean
Enter fullscreen mode Exit fullscreen mode

You might expect the type of xValue would be number and the one of yValue would be string. However, unfortunately, the TypeScript type system could not narrow down the returned type of the getProperty function. As a result, the type of the return value of the function has become a union type of the testObj properties.
If you want to make a more type-safe function, you should use a technique, Generic Constraints, and narrow down the type of the return value. Let me refactor the previous example code:

const testObj = { x: 10, y: "Hello", z: true };

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const xValue = getProperty(testObj, 'x');
// const xValue: number
const yValue = getProperty(testObj, 'y');
// const yValue: string
Enter fullscreen mode Exit fullscreen mode

As you saw, the types of xValue and yValue are narrowed down to a certain type of the testObj's properties.
In the above example code, the extends keyword works to constrain the type of the generic, K. In other words, one of the keys of T is chosen for K instead of allowing any keys to be the type of the key argument. Thus, the function can be limited to only one return type instead of a union type.

3. Conditional Types

TypeScript has the capability of making a logic to dynamically generate a type according to a type assigned to a generic.
Here is an example:

type IsString<T> = T extends string ? true : false;

type x = IsString<'hello'>;
// type x = true

type y = IsString<number>;
// type y = false

type z = IsString<string>;
// type z = true
Enter fullscreen mode Exit fullscreen mode

The IsString type is the so-called Conditional Type. The type of IsString differs according to its generic, T.
Let me interpret the line, T extends string ? true : false;.
If the type of T(left) is a subtype of string(right), in other words, if the T(left) type is assignable to the string(right) type, it returns true. Otherwise, it will be false.

Conclusion

In conclusion, the extends keyword in TypeScript is a versatile tool, playing crucial roles in interface inheritance, generic constraints, and conditional types. Mastering these concepts will help you write cleaner, more robust and more flexible TypeScript code.
However, at the same time, this keyword may confuse you when reading a codebase written by other developers since the extends keyword works differently according to contexts.
Therefore, you should keep in your mind the fact that the keyword has several meanings, "Inheritance", "Generic Constraints", and "Conditional Types".

Top comments (3)

Collapse
 
rei7 profile image
rei

thanks for this summary. knowledge learned.

Collapse
 
tomoy profile image
Tomohiro Yoshida

Thank you for your comment.
I am happy if my article helped you somewhat :)

Collapse
 
kela0 profile image
A2 D2

For generic constraints, "K extends keyof T" should mean that K is a subtype with type 'x'|'y'|'z'. Also keyof T also means that key is a type with 'x'|'y'|'z'
The way I see it, both are equivalent. Then why does the generic constraint code returns a more specific type?