TypeScript Types or Interfaces for React component props

reyronald

Ronald Rey

Posted on February 3, 2020

TypeScript Types or Interfaces for React component props

Photo by Simone Hutsch on Unsplash

When writing React components with TypeScript, you have two options when it comes to typing its props. You can use either type aliases or interfaces. Which one would you choose? Does it matter? Is there really an optimal choice? Are there any drawbacks to one or the other? Let's explore the relevant differences between them before we conclude.

This is not a deep case study of how types aliases and interfaces differ in TypeScript, but I'll provide just a brief overview of some of the differences that are relevant to React props so we're on the same page. Later we will explore how those differences can come into play in this context.

Type aliases vs. Interfaces

Type aliases and interfaces in TypeScript are equivalent in the majority of cases. Things that you can do in one you can also do with the other with just syntax changes, and of course, there are exceptions.

Let's take a look at some examples:

Regular object with properties

✔ Equivalent

type User = {
  name: string
}
// ...
interface User {
  name: string
}
Enter fullscreen mode Exit fullscreen mode

Arrays or indexers

✔ Equivalent

Arrays or indexers:

type Users = User[]
// ...
interface Users {
  [index: number]: User
}
Enter fullscreen mode Exit fullscreen mode

👆 In this case, though, the interface would be missing all array methods like .push, .map, etc. so both definitions won't exactly be equivalent, and the interface would be less useful unless that's precisely what you are aiming for.

To remediate this, you would have to explicitly extend from the array type like so:

type Users = User[]
// ...
interface Users extends Array<User> {
  [index: number]: User
}
Enter fullscreen mode Exit fullscreen mode

Functions

✔ Equivalent

type GetUserFn = (name: string) => User
// ...
interface GetUserFn {
  (name: string): User
}
Enter fullscreen mode Exit fullscreen mode

Function overloading with added properties

✔ Equivalent

Let's use a real-world example, this is the it: TestFunction type definition from mocha, see the source here.

type TestFunction = 
  & ((fn: Func) => Test)
  & ((fn: AsyncFunc) => Test)
  & ((title: string, fn?: Func) => Test)
  & ((title: string, fn?: AsyncFunc) => Test)
  & {
    only: ExclusiveTestFunction;
    skip: PendingTestFunction;
    retries(n: number): void;
  };
// ...
interface TestFunction {
    (fn: Func): Test
    (fn: AsyncFunc): Test
    (title: string, fn?: Func): Test
    (title: string, fn?: AsyncFunc): Test
    only: ExclusiveTestFunction
    skip: PendingTestFunction
    retries(n: number): void
}
Enter fullscreen mode Exit fullscreen mode

Although you can achieve this with both, I would recommend sticking to an interface in this case because of the clearer semantics and cleaner syntax.

Merging

✔ Equivalent

Merging properties from different types into one, referred to as intersections when using types aliases, or extensions when using interfaces.

type SuperUser = User & { super: true }
// ...
interface SuperUser extends User {
  super: true
}
Enter fullscreen mode Exit fullscreen mode
type Z = A & B & C & D & E
// ...
interface Z extends A, B, C, D, E {}
Enter fullscreen mode Exit fullscreen mode

There's one significant difference here that is not obvious from just looking at those examples. When extending interfaces, you absolutely need to declare a new one with the extension result, whereas with a type alias, you can inline the intersection type, for example:

function(_: A & B) {}
//...
interface Z extends A, B {}
function(_: Z) {}
Enter fullscreen mode Exit fullscreen mode

Class implementation

✔ Equivalent (!)

This might seem counter-intuitive, but you can implement both type aliases and interfaces in classes!

type AnimalType = {}
interface IAnimal = {}

class Dog implements AnimalType {} // ✔ Works
class Cat implements IAnimal {}    // ✔ Works
Enter fullscreen mode Exit fullscreen mode

However, although possible with both, this use-case is more commonly attributed to interfaces due to classic object-oriented language design, and it's safe to say you will rarely see types used this way in real-world codebases.

Union types

❌ NOT Equivalent

It is possible to define a type that is either one thing, or the other when declaring it as a type alias by using the union type syntax, but this is not possible with an interface:

type Z = A | B
//...
interface IZ extends A | B {} // <- ❌ INVALID SYNTAX, not possible to achieve this
Enter fullscreen mode Exit fullscreen mode

It is also not possible to extend from a type that is declared as a union type.

type Z = A | B

interface IZ extends Z {} // ❌ Compilation error:
// "An interface can only extend an object type or intersection
// of object types with statically known members."
Enter fullscreen mode Exit fullscreen mode

Redeclaration

❌ NOT Equivalent

There's another way of extending an interface definition. By redeclaring it, whatever is defined in the latest declaration will be merged with the properties of all previous declarations. So you can say that an interface's behavior is very similar to the cascading nature of CSS.

interface User {
  name: string
}

interface User {
  gender: string
}

const user: User = { name: 'Ronald', gender: 'male' }
Enter fullscreen mode Exit fullscreen mode

This is not possible to achieve with a type alias though:

type User = { name: string }
type User = { gender: string } // ❌ Compilation error
// "Duplicate identifier 'User'."
Enter fullscreen mode Exit fullscreen mode

This is particularly useful if you need to extend an existing object's definition whose type is declared outside of your reach, i.e. it's coming from a third-party package, or it's part of the standard library.

Imagine your web app adds a few properties to the window object. You won't be able to use your added properties without getting a compilation error because they won't be part of the original definition of the Window type. But since Window is declared as an interface, you can do this somewhere near the entry point of your client app:

declare global {
  interface Window {
    $: jQuery
  }
}

// ...

// and now you use $ globally without type errors
window.$; // 👍
Enter fullscreen mode Exit fullscreen mode

NOTE: This is not an encouragement to use jQuery.

Using in React props

With all those considerations in mind, which one would you say is the best choice for typing a React component's props? Is there a unique best practice? Can we say that using one or the other is an anti-pattern or should be avoided? Let's unpack.

When I see props declared with an interface, I immediately stop in my tracks and think: "Is it declared as an interface because the developer is going to be implementing it in a class later?", "Is it declared as an interface because the developer is going to redeclare it later, or is the possibility of redeclaration an intended feature of this component? If it is, how does this affect the usage of the component, if at all?"

I then proceed to start looking for the answer to these questions before I continue doing what I was doing, most times to little fruition because those were not factors involved in the decision to use an interface, but at this point, I've already wasted development time and more importantly precious scarce cognitive resources that I will never get back.

I don't ask myself those questions when I see a type alias though. A type alias feels like a more appropriate language construct for plainly defining what the shape of an object should look like and is more akin to functional-style programming, so it feels more at home with React given how React itself is a functional stab at designing user interfaces. An interface, on the other hand, has a lot of object-oriented baggage associated with it that is irrelevant when we specifically talk about React component props, and object-oriented programming is not React's paradigm.

Also, as you could see from the previous examples, types declarations are almost always more concise than their interface counterparts because of their syntax, and they can also be more composable thanks to the possibility of unions. If the prop object you are typing is really small you can get away with inlining it in the function declaration too, which you wouldn't be able to do if you are strictly sticking to interfaces.

Cool, does this mean I would always use a type for props, rather than interfaces? If you go and explore the type definition files for the most popular React re-usable component libraries you will notice that most of them use interfaces for props instead, so you could conclude that this is the globally accepted community approach for some reason.

When it comes to re-usable libraries, using interfaces instead is a very good and pragmatic choice as it allows the library itself to be more flexible because now each consumer can re-declare each of those interfaces as needed to add properties. This is useful because many OSS libraries separately maintain their type definitions from their sources so it's common for those definitions to get out of date, and when they do, users can easily get around it by leveraging the interfaces, and maintainers themselves are not bombarded with compilation related issue reports from the community.

But let's imagine a different scenario now. Imagine you are working in a multi-team company where many different teams work independently in their own front-end apps, but all depend on a private/internal re-usable component library that your team owns but everybody else contributes to. By nature, humans will always strive to find the path of least resistance to their goals. If you decided to use interfaces because of the reasons I stated above, it's very likely that when another team encounters a typing inconsistency issue they decide to quickly fix it in their codebases by leveraging the flexibility of extension points rather than contributing a fix upstream, and further fragmenting the consistency of the development experience across the company as a result.

In this case, I want to avoid providing too much extension or flexibility, and an interface's characteristics would be harmful.

Conclusion

So, what's my definitive answer? Type aliases or interfaces? My answer is: "I don't care" and "it depends".

Both types and interfaces are almost the same, and their fundamental differences are not that relevant for the super-simplistic case of React component props. Use whichever one you feel comfortable with unless there's a specific valid reason to use one over the other, like in the examples I laid out above.

The only thing I ask of you is that you don't mislead others into thinking that "you should always use types for React props" or "React props should always be declared with interfaces" and that one or the other is a "best practice" or "anti-pattern". All "best practices" are best practices because of several reasons that are situational and conditional and may not apply to all cases. Based on my experience, many engineers won't be brave or confident enough to challenge those assumptions and will go on with their lives living a lie that can affect their careers.

If you take away anything from this blog post is this:

  • Always challenge preconceived notions, assumptions and established "best practices".
  • Don't forget the reasoning behind best practices. And if you do, look them up before you use it in an argument or make a decision based on it.
  • If the line that divides many options is too blurry, the factors involved are too difficult to spot or are very trivial, don't waste the youth of your brain and go with whichever.
💖 💪 🙅 🚩
reyronald
Ronald Rey

Posted on February 3, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related