How To Use TypeScript Interfaces For Better Code Organization And Readability

Olasunkanmi Balogun - Feb 18 '23 - - Dev Community

TypeScript is an add-on for JavaScript. That is, it includes more features and improvements such as syntax for types. TypeScript runs with a compile-time type checker hence you can avoid or debug errors faster when you add types to your code. According to its official website, it is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.

In TypeScript, there are several ways to represent objects, one of those ways includes interfaces. An interface controls the property names an object can have, this means that when an object implements an interface, the object conforms to the rules specified by the interface.

Interfaces can help achieve code organization and readability which is crucial for maintaining, collaborating, reusing, and optimizing your codebase. In this article, we'll discuss:

  1. Importance Of Interfaces
  2. Syntax And Basic Use Cases Of Interfaces
  3. Conclusion

Importance Of Interfaces

Interfaces provide a range of benefits, from improving code consistency and readability to facilitating reuse and catching errors early in the development process. In this section, we'll discuss a few importance of using interfaces in your TypeScript code.

  1. Enforce consistency: Interfaces let you define a common set of properties and methods that objects must implement, ensuring that the structure and behavior of objects of a given type are the same.

  2. Facilitate documentation: Interfaces serve as a place to document the expected properties and methods of an object.

  3. Improve Readability: By providing the expected structure of an object, including its properties, methods, and types, interfaces make code easier to read especially when working with complex codebases or collaborating with other developers.

  4. Catch errors early: Enforcing strict type-checking at compile time, interfaces help catch errors before they make it to production. This can save time, effort, and cost.

  5. Code Reusability: Since interfaces allow you to define custom types that can be used in place of built-in types like number and string, it can ensure that code is more reusable thereby reducing duplication and also make the code modular - this means you can export and import it.

Syntax And Basic Use Cases Of Interfaces

In this section, we'll get started with interfaces in TypeScript. It's important to understand their syntax and basic use cases - defining and implementing interfaces, and how to use them to enforce type safety in your code.

NOTE: To follow along with the code snippets provided, you can use the official TypeScript playground.

Syntax

Interfaces are defined using the interface keyword followed by its name, then the body of the interface in curly braces. Here's an example of an interface.

interface User {
firstName: string;
lastName: string;
}
Enter fullscreen mode Exit fullscreen mode

We defined an interface and named it User, it has properties firstName and lastName, both of which are of type string.

We can now use the User interface as a natural data type.

interface User {
firstName: string;
lastName: string;
}

//define object that conforms to the specification of interface User

const user: User = {
  firstName: 'John',
  lastName: 'Doe'
}
Enter fullscreen mode Exit fullscreen mode

Objects using the User interface as their type must have the same properties(the keys in the object), as specified in the User interface. If a property is optional, it can be omitted. If one of the properties is missing or if its value isn't the same data type specified in the interface, then the compiler will throw an error. Hence we cannot add extra fields that are not defined in the interface to the object.

Let's try to add a new property that was not initially defined in the interface and see what happens:

interface User {
firstName: string;
lastName: string;
}

const user: User = {
  firstName: 'John',
  lastName: 'Doe',
  id:'123' //new property
}
Enter fullscreen mode Exit fullscreen mode

The above code will cause the TypeScript compiler to throw an error 2322, since the id property was not initially declared in the interface:

Type '{ firstName: string; lastName: string; id: string; }' is not assignable to type 'User'.
Object literal may only specify known properties, and 'id' does not exist in type 'User'.(2322)
Enter fullscreen mode Exit fullscreen mode

Basic Use Cases

Now that we already know how interfaces are defined, let's discuss a few use cases.

Use Case: Optional Properties

To make a property optional as discussed earlier, you append ? to its name as seen below:


interface User {
firstName: string;
lastName: string;
id?: string; //id can now be omitted and the compiler won't throw any errors
}

const user: User = {
  firstName: 'John',
  lastName: 'Doe',  
}

Enter fullscreen mode Exit fullscreen mode

Use Case: Read-only Properties

In cases where we want to make a property read-only in an interface, we include the readonly keyword before the name of the property:

interface User {
firstName: string;
lastName: string;
id?: string;
readonly dbid: number; 
}

const user: User = {
    firstName: 'John',
    lastName: 'Doe',
    dbid: 22
}
Enter fullscreen mode Exit fullscreen mode

After dbid's first assignment in the user object, it cannot be reassigned again. Using the readonly keyword with a property means the property cannot be modified after its first assignment. You can think of its behavior as the same as the const keyword in JavaScript. Hence, the code below will throw an error 2540:

interface User {
firstName: string;
lastName: string;
id?: string;
readonly dbid: number; 
}

const user: User = {
    firstName: 'John',
    lastName: 'Doe',
    dbid: 22
}

user.dbid = 4;
Enter fullscreen mode Exit fullscreen mode

Error message:

Cannot assign to 'dbid' because it is a read-only property.(2540)
Enter fullscreen mode Exit fullscreen mode

Use Case: Extending Other Types

When creating new interfaces, we can extend our interfaces so they include properties from other interfaces or types. This way you can extract interfaces that contain the same properties and use them as components for other interfaces.

From the User interface in previous examples, let's define a new interface Admin that also contains the firstName and lastName properties but also contains a new property role. We can simply extend the Admin interface from the User interface.

See code example below:


interface User {
firstName: string;
lastName: string;
id?: string;
readonly dbid: number; 
}

interface Admin extends User {
   role: 'admin' | 'user' | 'developer' 
}

const admin: Admin = {
    firstName: 'John',
    lastName: 'Doe',
    dbid: 22,
    role: 'admin'
}

Enter fullscreen mode Exit fullscreen mode

The Admin interface now inherits the properties of the interface User and also includes its own property role. It's just as if we did:


interface Admin {
firstName: string;
lastName: string;
id?: string;
readonly dbid: number; 
role: 'admin' | 'user' | 'developer' 
}

Enter fullscreen mode Exit fullscreen mode

Extending an interface is not just limited to other interfaces. They can extend from any object type like normal types, and also classes.

Use Case: Function Types

We can use interfaces to describe function types. That is, we can define the expected parameter type and expected return type of a function in an interface.

See code example below:

interface AddFunction {
  (a: number, b: number): number;
}

const add: AddFunc = (a, b) => {
  return a + b;
}

Enter fullscreen mode Exit fullscreen mode

We defined an interface AddFunction that describes how the parameters and return value of any function that utilizes the interface should look like. You see that after we declared the interface, we didn't need to specify the parameter and return type in the add function anymore.

Note: The names of the parameters in our function do not need to match the ones defined in our function type.

Use Cases: Index Signatures

Index signatures are a way of defining the shape of an object with dynamic property names or when the properties of an object are not known yet.

In the code below, we'll see how we can define an index signature for an object whose properties we don't know yet:

interface Score {
  [key: string]: number;
}

const score: Score = {
  score1: 1,
  score2: 2,
  score3: 3
}

Enter fullscreen mode Exit fullscreen mode

In the above example, we defined an interface Score that has an index signature. The key is a string, and the value is a number. This simply means that any object(like the score object above), that utilizes the interface must have its keys as type string and values as type number.

Conclusion

While we've discussed some common use cases of interfaces, you should not that there are many more use cases of interfaces in TypeScript. Whether you're working on a large codebase with multiple developers or you just want to improve the quality of your own code, interfaces can be a valuable tool to have in your knowledge base.

If you're interested in learning more about interfaces in TypeScript, you can explore the documentation and start experimenting with different use cases.

In conclusion, interfaces are a powerful feature of TypeScript that can provide a range of benefits, from improving code consistency and readability to facilitating reuse and catching errors earlier in the development process. By enforcing consistent types across a codebase, interfaces can help ensure that code is easier to read and understand, more modular and reusable, and less error-prone.

If you have any questions, feel free to ask them in the comment section.

. . . . . . . . . . .