Mastering the extends Keyword in TypeScript

Table of Content
- 1. Using extends with Class Inheritance
- 2. Constraining Generics with extends
- Example: Constraining to Specific Properties
- Example: Restricting Generics to Specific Union Types
- 3. Using extends in Conditional Types
- Syntax of Conditional Types
- Example: Building a Simple Conditional Type
- 4. Extending Interfaces
- Example: Extending a Base Interface
- Key Benefits of Extending Interfaces
- Summary
The extends
keyword in TypeScript is a powerful tool for enhancing and controlling types. Although the word "extends" often brings inheritance to mind, TypeScript broadens its use to include constraining types, building on existing interfaces, and creating flexible generic and conditional types. This article covers how extends
can help you create scalable, reusable, and type-safe TypeScript code.
1. Using extends with Class Inheritance
In TypeScript, extends
can be used to establish inheritance in classes, just as it does in JavaScript. When a class extends another, it inherits the properties and methods of the parent class.
class Animal {
speak() {
console.log("The animal speaks.");
}
}
class Dog extends Animal {
bark() {
console.log("The dog barks.");
}
}
const myDog = new Dog();
myDog.speak(); // "The animal speaks."
myDog.bark(); // "The dog barks."
In this example:
Dog
inherits all methods and properties ofAnimal
.- We can call
speak()
on an instance ofDog
, even thoughspeak
is defined inAnimal
.
Using extends
in this way provides the benefits of code reuse and allows for object-oriented design patterns.
2. Constraining Generics with extends
Another powerful application of extends
in TypeScript is to constrain generic types. Generics allow you to create flexible and reusable components that can handle multiple types, but sometimes you need to restrict which types can be passed as generics. extends
allows you to enforce such constraints.
Example: Constraining to Specific Properties
Consider a function that logs the length of an item. By using extends
, we can constrain the generic type to only types that include a length
property.
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
// Valid usage
logLength("Hello, TypeScript!"); // Output: 16
logLength([1, 2, 3, 4, 5]); // Output: 5
// Invalid usage
logLength(123); // Error: number doesn't have a length property
In this example:
T
can be any type that has alength
property, likestring
,array
, or a custom object withlength
.- Attempting to call
logLength
with a type that doesn’t havelength
(e.g.,number
) results in a TypeScript error.
Example: Restricting Generics to Specific Union Types
You can also use extends
to limit a generic type to specific literal values, useful for functions that should accept only a restricted set of values.
type AnimalType = "cat" | "dog";
function makeSound<T extends AnimalType>(animal: T) {
if (animal === "cat") {
console.log("Meow!");
} else {
console.log("Woof!");
}
}
makeSound("cat"); // Meow!
makeSound("dog"); // Woof!
// makeSound("bird"); // Error: Argument of type '"bird"' is not assignable to parameter of type 'AnimalType'.
3. Using extends in Conditional Types
Conditional types, introduced in TypeScript 2.8, provide an advanced way to return different types based on a condition. Conditional types use extends
to evaluate a condition and decide on the resulting type.
Syntax of Conditional Types
T extends U ? X : Y
- If
T extends U
(i.e.,T
is compatible withU
), then the type resolves toX
. - Otherwise, it resolves to
Y
.
Example: Building a Simple Conditional Type
Let’s say we want to create a type that returns "yes"
if a type is string
and "no"
otherwise.
type IsString<T> = T extends string ? "yes" : "no";
type Test1 = IsString<string>; // "yes"
type Test2 = IsString<number>; // "no"
In this example:
IsString
checks ifT extends string
. If so, it resolves to"yes"
; otherwise, it resolves to"no"
.
4. Extending Interfaces
Interfaces are often used to define the structure or "shape" of objects, and extends
allows us to build on existing interfaces. By extending interfaces, you can reuse and share common properties across multiple interfaces, making your code more modular and manageable.
Example: Extending a Base Interface
Consider an application with multiple entities (User
, Post
, and Comment
) that each have a common id
property. Instead of adding id
to each interface separately, we can define a Base
interface and have each interface extend it.
interface Base {
id: string;
}
interface User extends Base {
firstName: string;
lastName: string;
}
interface Post extends Base {
title: string;
body: string;
}
interface Comment extends Base {
comment: string;
}
User
,Post
, andComment
all extendBase
, inheriting theid
property.- This approach simplifies updates: modifying
Base
will update all interfaces that extend it.
Key Benefits of Extending Interfaces
- Single Source of Truth: Changes to shared properties only need to be made in one place.
- Modularity: Types are easier to scale as your application grows.
- Code Reusability: Common properties or behaviors are easily shared across multiple interfaces.
Summary
The extends
keyword in TypeScript is a versatile tool that goes beyond traditional inheritance. It can be used to extend interfaces, constrain generic types, and enable complex type transformations through conditional types. By mastering extends
, you gain the ability to create flexible, reusable, and type-safe TypeScript code structures that support complex type scenarios and contribute to cleaner, more maintainable applications.