More accurate the type, better the code

macsikora

Pragmatic Maciej

Posted on July 24, 2019

More accurate the type, better the code

Imagine that somebody gives you many unknown objects in black boxes, one by one. You cannot know what is in the box until you ask correct questions. As for an orange, you would ask if it is a fruit, and if it has orange color. And if both answers are true, then probably it is an orange inside. After verifying the object you pass it to the next person in the same black box it was given to you. The next person needs to figure out the object again as there is still no information about it, only the same black box.

This is exactly how functions work with data structures in dynamic type language like JavaScript. Till you put a condition, it can be anything. And even if you ask, the details like - object properties, remain unknown. That is exactly why, in plain JS there are a lot of defensive checks everyplace, as the contract remains unknown, even if some other function checked that before.

Less you know, more you ask

In real life we need to examine the object to understand what can be done with it, we use our human memory and brain specialized in identification of familiar things. Would you grab something into your hand without knowing and seeing what it is? It would be quite risky, as it could be for example a knife.

And the same knowledge demand applies to programming. Broad type, or no type, gives more questions than answers. So if you have many questions, the code need to ask them every time. And asking means - conditions. How you would work with such broad and not framed type:

interface Something {
  type: string;
  maxSpeed?: number;
  cookingTime?: number;
  wheelSize?: number;
  name?: string;
  lastname?: string;
  carModel?: string;
  age?: number;
  ...
}
Enter fullscreen mode Exit fullscreen mode

It would be just a nightmare, and even when in the code, you would know, that you currently deal with some car, you can still ask about this car cookingTime or lastname :). Above is exact opposite of a good type definition - broad with many optional fields. Another thing is that nobody should ever create such polymorphic structure. And the impact on the code is not neutral, there would be plenty of conditions in every place, and most of these conditions will be done in circumstances where they have no sense.

The real broad type

Let's switch to some real example, I will change the domain into beloved server response structure, with which everyone in some time needs to work. I will assume that our middleware responsible for the communication with the server, models the response in a such way:

interface ServerResponse {
  code: number;
  content?: Content;
  error?: Error;
}
Enter fullscreen mode Exit fullscreen mode

Yes we have it, nice type I could say, better at least from the previous one. But also we know something more, that specific response codes have specific implication on other fields. And exactly these relations are:

  • for error codes like - 500 and 400 there is the error field but no content
  • for 200 code there is the content but not the error
  • for 404 there is no content and no error

The type then, has hidden dependencies and can represent not possible shapes. Hidden dependency exists between property code and properties content and error.

const resp = getResponse()
if (resp.code === 500) {
  console.log(resp.content && resp.content.text); // there never can be the content property
}
Enter fullscreen mode Exit fullscreen mode

This condition is a valid question from the type perspective, as the type doesn't say nothing about fields relation, but in reality it cannot happen. Furthermore, even if you know, that there is always the error field, there always needs to be defensive check, as the type just doesn't represent that:

const resp = getRespomse()
if (resp.code === 500) {
  console.log(resp.error && resp.error.text); // the error property will be there always
}
Enter fullscreen mode Exit fullscreen mode

The type is too broad

What to do then. You can just write the code and avoid this kind of things by reaching to your own human memory or some kind of documentation, which soon will be outdated. In other words these rules will remain as the tribal knowledge of this project, and ones per a while somebody will ask - why 404 has no error property set, and why somebody checks existing of content in the error response.

Project Shaman will help you :)

Project Shaman will help you :)

Or instead of that, you can properly model these relations in types. And the good information is - in TypeScript you can nicely do that.

Put the knowledge into the type

Let's try to form the types in the correct, narrow way. For the example purposes I will simplify and say that the server can send only 500, 400, 404 and 200 http codes. Then I can extract below types:

interface SuccessResponse {
  code: 200;
  content: Content;
}

interface ErrorResponse {
  code: 400 | 500;
  error: Error;
}

interface NotFoundResponse {
  code: 404;
}
Enter fullscreen mode Exit fullscreen mode

Great! Now I have three not related types. But response can be or Success or Error or NotFound. And exactly that I will do, I will join them by union:

type ServerResponse = SuccessResponse | ErrorResponse | NotFoundResponse
Enter fullscreen mode Exit fullscreen mode

And done! Yes that is the whole thing. Now all relations between code and other properties are in the type. There is no way to use content in ErrorResponse or error in SuccessResponse, or any of them in NotFoundResponse. If I try to create invalid object, compiler will scream. Also the code field was narrowed from broad number type into only few specific possibilities.

What's more, after checking of status code, TypeScript will automatically narrow the type in the scope. So if you check:

if (response.code === 500) {
  // here only `error` property is accessible
  console.log(response.error.text)
}

if (response.code === 200) {
  // here only `content` property is accessible
  console.log(response.content.text)
}

if (response.code === 404) {
  // here no additional properties are available
}

Enter fullscreen mode Exit fullscreen mode

Moreover these conditions don't need to be used directly. Additional abstraction in form of functions will be far more handy to use:

// declaration of the type guard function
const isErrorResponse = (response: Response): response is ErrorResponse => response.code === 500 || response.code === 400;

// using
if (isErrorResponse(resp)) {
  // in this scope resp is type of ErrorResponse
}

Enter fullscreen mode Exit fullscreen mode

More accurate the type, better the code

What I did is narrowing the type, this is exactly what you should do when working with static type language. As types are documentation and the code guide, having them accurate is just in your interest. The pattern I have describe here has a name - it is Discriminated Union or Tagged Union. Check it out in the official TS documentation. See you next time!

πŸ’– πŸ’ͺ πŸ™… 🚩
macsikora
Pragmatic Maciej

Posted on July 24, 2019

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

Sign up to receive the latest update from our blog.

Related