Understanding TypeScript Interfaces, Classes, and Abstract Classes
Understanding TypeScript Interfaces, Classes, and Abstract Classes
Interface, class, and abstract class are three fundamental concepts in TypeScript that have both connections and distinctions. This article explores the relationships between these constructs based on TypeScript's official documentation.
- Declaration Types in TypeScript
When working with TypeScript, it's essential to understand what diffferent declarations do. The following table categorizes various TypeScript declarations by their namespace, type, and value capabilities:
| Declaration Type | Namespace | Type | Value |
|---|---|---|---|
| Namespace | X | X | |
| Class | X | X | |
| Enum | X | X | |
| Interface | X | ||
| Type Alias | X | ||
| Function | X | ||
| Variable | X |
Let's examine what happens when we declare each of these constructs:
namespace utility {
calculate(): number;
}
class Person{
fullName: string;
}
let userInstance = new Person();
enum Status {active, inactive, pending}
interface productSpec {
manufacturer: string;
cost: number
}
type eventType = 'click' | 'scroll';
function process(){}
let x = 5;
var y = {};
const z = null;
A namespace declares both a namespace and a value. For example, libraries like Lodash use namespaces to organize utility functions under a single namespace (often _).
A class declaration creates both a type and a value. You can assign an instance of a class to a variable (making it a value) and also use the class name as a type annotation.
An enum declaration similarly creates both a type and a value. You can reference enum members as types and also assign the enum itself to variables.
An interface, however, only declares a type. You cannot assign an interface to a variable as it doesn't represent a runtime value.
Function, variable, and constant declarations only create values and not types. You cannot use them as type annotations.
- Interfaces and Classes
JavaScript has six primitive types: number, string, boolean, null, undefined, and object. However, these are often insufficient for describing the shape of complex data structures that functions should accept. This is where interfaces come in - they define the shape of values.
Classes in TypeScript also have the capability to describe shapes (types), but they additionally provide implementation details and can be instantiated.
Classes can implement interfaces:
interface Vehicle {
move(): void;
wheels: number;
doors: number;
}
class Car implements Vehicle {
wheels: number = 4;
doors: number = 4;
move() {
console.log('The car is moving');
}
}
Interfaces can extend classes, treating the class as a type:
interface ElectricCar extends Car {
batteryCapacity: number;
}
However, interfaces cannot extend enums or type aliases becuase they don't represent object types with statically known members. TypeScript provides the following error:
An interface can only extend an object type or intersection of object types with statically known members.
- Interfaces and Type Aliases
In most cases, interfaces and type aliases can be used interchangeably. However, there are two key differences:
- Type aliases can rename primitive types, while interfaces cannot:
type Numeric = number;
// This would cause an error with an interface
- Interfaces support declaration merging, while type aliases do not:
interface Animal {
species: string
}
interface Animal {
habitat: string
}
// These two interfaces are merged into one type with both properties
const creature: Animal = {
species: "Mammal",
habitat: 'Land'
}
type Plant = {
family: string
}
// This would cause an error - type aliases cannot be merged
type Plant = {
genus?: string
}
This difference can lead to interesting situations. For example, when working with index signatures:
// Base type
type DataStructure = Record<string number="" string="">;
function processData(data: DataStructure) {}
// First approach with type alias
type UserProfile = {
id: string;
};
let userProfileData: UserProfile = {
id: '12345',
};
processData(userProfileData);
// Second approach with interface
interface UserProfileInterface {
id: string;
}
let userProfileInterfaceData: UserProfileInterface = {
id: '12345',
};
// This causes an error because the interface lacks an index signature
processData(userProfileInterfaceData);
// The solution is to add an index signature
interface UserProfileWithIndex {
[k: string]: string | number;
id: string;
}
</string>
The index signature [k: string]: string | number ensures that any additional properties added to the interface through declaration merging will conform to the expected type.
- Classes and Abstract Classes
The primary distinction between classes and abstract classes is that abstract classes cannot be instantiated:
abstract Animal {
name: string;
abstract makeSound(): void;
describe() {
return `<animal:>`
}
}
new Animal // Error: Cannot create an instance of an abstract class.
</animal:>
- Interfaces and Abstract Classes
Both interfaces and abstract classes cannot be instantiated. However, abstract classes can be assigned to variables, while interfaces cannot.
Another significant difference is that interfaces cannot contain method implementations, while abstract classes can provide partial implementations that can be utilized by subclassses.