Published on: 06/06/2024
Written by James Bridge
TypeScript provides several ways to define custom types: interfaces, type aliases, and enums. Each has its own use cases and strengths. Let’s dive into each one and explore when to use them.
Interfaces in TypeScript define the structure of objects. They’re primarily used for type-checking and to ensure that objects adhere to a specific shape.
interface User {
id: number;
name: string;
email: string;
age?: number; // Optional property
readonly createdAt: Date; // Read-only property
}
function createUser(user: User): void {
// Implementation
}
createUser({
id: 1,
name: "John Doe",
email: "john@example.com",
createdAt: new Date()
});
Defining object shapes: Use interfaces when you need to define the structure of objects, especially for complex structures.
Class contracts: Interfaces are great for defining contracts that classes should adhere to.
Extending and implementing: Interfaces can extend other interfaces and can be implemented by classes.
Declaration merging: If you need to add properties to an interface later, you can redeclare it, and TypeScript will merge the declarations.
Type aliases create new names for types. They can represent not just object types, but also primitives, unions, tuples, and more.
type ID = string | number;
type Point = {
x: number;
y: number;
};
type UserRole = "admin" | "user" | "guest";
type Callback = (error: Error | null, data: any) => void;
const id: ID = "abc123";
const role: UserRole = "admin";
Unions and intersections: Types are great for creating union types (|
) or intersection types (&
).
Complex types: Use types for more complex type definitions, like mapped types or conditional types.
Aliases for primitives: When you want to give a meaningful name to a primitive type or a union of primitives.
Tuples: Types can be used to define tuple types.
Function signatures: When you want to create an alias for a function signature.
Enums allow you to define a set of named constants. They make it easier to document intent or create a set of distinct cases.
enum Direction {
North,
South,
East,
West
}
enum HttpStatus {
OK = 200,
NotFound = 404,
InternalServerError = 500
}
function move(direction: Direction): void {
// Implementation
}
move(Direction.North);
const status: HttpStatus = HttpStatus.OK;
Distinct values: When you have a set of related constants that are known at compile time.
Improved readability: Enums make your intentions clearer and your code more readable.
Type safety: Enums provide type safety when you want to limit a variable to only a few possible values.
Reverse mapping: Numeric enums provide reverse mapping, allowing you to get the enum member name from its value.
While interfaces and types are similar in many ways, there are some key differences:
Extensibility: Interfaces can be extended or implemented, while types cannot.
Declaration merging: You can add new properties to an interface by redeclaring it, but not to a type.
Computed properties: Types can use computed properties, while interfaces cannot.
type Keys = "firstname" | "surname";
type DudeType = {
[key in Keys]: string;
};
// This is not possible with interfaces
Use interfaces for defining public APIs or when you need to take advantage of declaration merging.
Use types for complex type definitions, unions, intersections, or when you need mapped or conditional types.
Use enums when you have a fixed set of related constants.
Prefer interfaces over types for object shapes when possible, as they’re more familiar to OOP developers and provide better error messages in some cases.
Use type aliases for function types and simpler object types, especially when you need unions or intersections.
By understanding the strengths and use cases of interfaces, types, and enums, you can write more expressive and type-safe TypeScript code, improving code readability and maintainability.