Mastering TypeScript: A Guide to Choosing Between ‘type’ and ‘interface’
In the TypeScript realm, 'type' and 'interface' play pivotal roles. This article breaks down their distinctions and offers insights on when to leverage each, ensuring more robust TypeScript code.
TypeScript has become a reliable tool for catching errors and improving the stability of code. However, when working with TypeScript, I’ve often wondered about the differences between the type
and interface
keywords and when to use each one.
In this blog post, I want to share what I’ve learned about the differences between type
and interface
and when to use each one. I'll provide clear examples and explanations to help you better understand how to use these two powerful tools in your TypeScript projects.
By the end of this post, you’ll have a better understanding of the differences between type
and interface
and how to use them effectively in your TypeScript code. I hope this post will be helpful to other web developers who are looking to improve their TypeScript skills and write more reliable code.
Type vs Interface
In TypeScript, both type
and interface
can be used to define object types, but they have some differences in syntax and functionality. interface
is a keyword used to define object types and has the syntax.
interface IUser {
name: string;
age: number;
...
}
On the other hand, type
is a keyword used to define object types and has the syntax:
type User = {
name: string;
age: number;
...
};
The syntax difference is minor, but there are other differences in functionality.
interface
can be extended to create new types that inherit the properties of the parent interface:
type Union = Type1 | Type2;
type Intersection = Type1 & Type2;
These types allow you to define more complex relationships between types and can help you model data more accurately.
Example — Building a Messaging App
Let’s say you’re building a messaging app, and you want to define a type for a message that can contain either text or an image. You can use a union type to define this:
type Message = {
type: 'text' | 'image';
content: string | File;
}
In this example, we use type
to define a union type that can only be one of two string values: 'text'
or 'image'
. We also use a union type to define the content
property, which can be either a string (for text messages) or a File
object (for image messages).
Now let’s say you want to define a type for a group of users who have different roles in your app, such as admin, moderator, and regular user. You can use an intersection type to define this:
type User = {
id: number;
name: string;
}
type Admin = User & {
role: 'admin';
permissions: string[];
}
type Moderator = User & {
role: 'moderator';
canDelete: boolean;
}
type RegularUser = User & {
role: 'user';
isPremium: boolean;
}
In this example, we define a User
type with two properties: id
and name
. We then define three other types (Admin
, Moderator
, and RegularUser
) that extend the User
type using an intersection type (&
). Each of these types has a different role
property and additional properties specific to their role.
Using union and intersection types with type
can help you define more complex types in your TypeScript code and make it easier to model real-world data.
Declaration Merging
interface
also supports declaration merging, which allows you to define multiple interfaces with the same name and merge their properties into a single interface:
interface MyObject {
property1: Type1;
}
interface MyObject {
property2: Type2;
}
const myObject: MyObject = {
property1: 'value1',
property2: 'value2'
};
This can be useful when you’re working with third-party libraries or systems that define their interfaces separately and need to merge them into a single interface.
Example — Unleashing the Power of Declaration Merging
Let’s say you’re building an app that requires a user to sign in and you want to define an interface for the user object returned by your authentication API. You might define the interface like this:
interface User {
username: string;
email: string;
}
Later, you find out that the authentication API also returns a userId
property for each user, but you don't want to modify the original User
interface. Instead, you can define a new interface with the same name and it will automatically merge the properties with the original User
interface:
interface User {
userId: number;
}
// Now the User interface has three properties: username, email, and userId
const user: User = {
username: 'john.doe',
email: 'john.doe@example.com',
userId: 123
};
In this example, we define the User
interface with two properties: username
and email
. We then define a new User
interface with the userId
property. When we use the User
interface to define our user
object, the properties from both interfaces are merged together to create an object with three properties: username
, email
, and userId
.
Declaration merging is a useful feature of interface
that allows you to extend existing interfaces without modifying their original definition.
Let’s talk about the Open/Closed Principle
Ah, the Open/Closed Principle (OCP). One of the five SOLID principles of object-oriented programming design. It’s like the holy grail of software development. Everyone talks about it, but do they really understand it? Or are they just pretending?
I hope you remember: SOLID is an acronym that stands for five principles of object-oriented programming design that were introduced by Robert C. Martin (also known as Uncle Bob). These principles are intended to make software development more scalable, maintainable, and extensible.
The previous example is a good illustration of the Open/Closed Principle because it shows how you can extend an interface without modifying its original definition. By defining a new interface with the userId
property, rather than modifying the original User
interface, you're keeping the original interface closed for modification while still extending its functionality. This approach makes it easier to maintain and evolve your code over time without introducing unexpected issues or breaking changes.
But wait, there’s more! If you use TypeScript or another language with declaration merging, you can have your cake and eat it too. You can define the User
interface with just two properties, and then define a new User
interface with the userId
property. When you use the User
interface to define your user object, the properties from both interfaces magically merge together to create an object with three properties. It's like magic, but with less rabbits and more TypeScript.
But let’s be real, who has time for all this OCP nonsense? Sometimes you just need to get stuff done, and modifying existing code is the fastest way to do it. Who cares about the OCP anyway? It’s just a fancy principle that software developers like to throw around to make themselves sound smart.
So, go ahead and violate the OCP if you must. Just make sure you have a good reason for doing so. And if anyone asks, just tell them you’re following the KCP (Keep it Complacent Principle). That’s a principle we can all get behind.
React Component Props
So you’re wondering whether to use types or interfaces to define your React component props? Well, let me tell you, you’ve come to the right place. I mean, who needs a life when you can spend your days debating the finer points of TypeScript syntax? You’re like a modern-day Socrates, asking the important questions like “should I use type
or interface
?" while the rest of us plebs are busy swiping left on Tinder. So buckle up, my friend, because we're about to dive deep into the exciting world of type definitions for React components.
Using type
or interface
to define props in a React component is largely a matter of personal preference, as both options provide similar functionality. However, there are a few differences to consider when choosing between them.
interface
is a commonly used approach for defining props in React components. This is because interface
allows you to define the shape of the props object in a way that makes it easy to extend and reuse. For example, you can create a base Props
interface that defines the common props for a group of components and then extend it for each individual component as needed:
interface Props {
className?: string;
onClick?: () => void;
}
interface ButtonProps extends Props {
type?: 'button' | 'submit' | 'reset';
}
function Button({ className, onClick, type }: ButtonProps) {
// render button with props
}
In this example, we define a Props
interface with two optional props: className
and onClick
. We then extend the Props
interface to create a ButtonProps
interface that adds an optional type
prop. We can then use the ButtonProps
interface to define the props for our Button
component.
type
, on the other hand, is useful when you need to define more complex types, such as union or intersection types, in your props. For example, you can use type
to define a Size
type that represents different size options for a component:
type Size = 'small' | 'medium' | 'large';
interface Props {
size: Size;
}
function Component({ size }: Props) {
// render component with props
}
In this example, we use type
to define a Size
type that represents the different size options for our component. We then use the Props
interface to define the size
prop, which is required and can only be one of the values in the Size
type.
In general, using interface
is a good choice for defining simple props that can be extended or reused, while using type
is a good choice for defining more complex types in your props.
In conclusion, both type
and interface
can be used to define props in a React component, but they have some differences in functionality. interface
is useful for defining simple props that can be extended or reused, while type
is useful for defining more complex types in your props. Ultimately, the choice between type
and interface
is a matter of personal preference and the specific needs of your project.
Summary
In general, interface
is often used to define the shape of objects that are used as interfaces with other systems or libraries. type
, on the other hand, is often used to define more complex types, such as unions, intersections, or mapped types, or to define types that cannot be expressed as interfaces, such as function types or conditional types.
Cheers!