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'.
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');
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
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
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
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)
thanks for this summary. knowledge learned.
Thank you for your comment.
I am happy if my article helped you somewhat :)
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?