TL;DR: the Interface Segregation is particularly smooth in TypeScript. Use it.

When I write a function, I want to declare its parameter types to be the minimum of what the function needs. It may be easier to use the same type as what gets passed in, because then I don't need to declare a new type. But sometimes it's worth the extra work.

When I make a UI component, I like it to declare exactly what it is going to use in the display. So I create a custom, local type (for its React props, in this case) that declares exactly the fields it uses, and nothing else. For example:

export interface AspectForDisplay {
    name: string;
    displayName: string;
    documentationUrl?: string;
}
source

Now, it happens that the type that gets passed in is a ManagedAspect, which extends BaseAspect, which has these and other props. It would be easier (less typing) to declare that as the prop passed in. But it would be less specific. Fortunately I'm in TypeScript

TypeScript does duck typing, so as long as whatever's passed in has the properties that I declare (and they have the correct types), it's fine. So I pass a ManagedAspect into a function that requests an AspectForDisplay. That helps a lot; in a language like Java or Scala I'd have to go back and make ManagedAspect implement AspectForDisplay explicitly, and no way am I letting the UI intrude into the analysis code like that.

Even better (as Rod points out), TypeScript offers another solution. Instead of declaring another interface, I can express this type in terms of the one passed in.

type AspectForDisplay = Pick<BaseAspect, "documentationUrl" | "name" | "displayName">;
source

This says, "Take the properties out of type BaseAspect. Find the ones called either documentationUrl or name or displayName. Put them in this new type." This gives AspectForDisplay the three properties I want, with the same types, optional-ness, and readonly-ness they have in BaseAspect.

I love this syntax because: it tells me that these properties have the same meanings they have in BaseAspect. Even though they're all strings, the documentationUrl is a specific kind of string (a URL, yeah); the displayName is an ordinary string, since it's for display; and the name has technical meaning (it is a foreign key, effectively).

Being specific about what my function needs has some advantages:

  • it's informative. By looking at the parameters, people can understand what information the function accesses.
  • it's way more testable. I can call it without declaring all the irrelevant fields in BaseAspect just to pacify the compiler.
  • when someone adds a field to BaseAspect, it won't change the API to this function. They won't have to change tests of my function.

It has some negatives:

  • more work, initially. I have to type push more buttons on the keyboard.
  • when I want to use an additional field, it takes an extra change. That can be a breaking change if this function is a public API and that field is required in BaseAspect.

There are places where these negatives are sufficient to deter me. Local functions that I extracted in order to give them a name, it is rarely worth making a new type.

But along conceptual boundaries, like between the result of data analysis and the UI, this is particularly valuable. In any code I want to re-use, this is valuable. In any code I want to unit test, yes please.

For interviewing purposes, this is called the Interface Segregation Principle, and it's the I in "SOLID," from back in the day when Object Oriented programming was king. This principle works for all statically-typed programming, especially functional programming. It's goes with Postel's Law: be liberal in what you accept.

Be specific about what you need, not what you get.