Explore Your Types

Don’t worry, this isn’t a ‘what type of sandwich are you?’ kinda post. Instead we’ll look at how we can safely add types to our legacy code.

Adding types

There are really two ways of adding types. You either declare what you want your types to be, or you declare what the types can be. When writing new code we should always be precise in our types, so we declare what we want. We can say that a property is a string, or that a method only takes a User class as a parameter.

But for our legacy code, this is a bit different. Lets take, for example this snippet from a Login class i recently had to deal with. While password is annotated as a string, when we look at the implementation, we see that false is also an option.

class Login
{
  /** 
   * @var string
   */
  private $password;

  public function login($email, $password = false)
  {
    // stuff
    $this->password = $password;
  }
}

Tools like Psalm and PHPStan will tell us there is an error here, or you could find it on your own. This is a problem we’d like to solve, but there are multiple ways of doing so. One would be to add a (string) cast, before the assignment of $password. The problem with that, is that we are changing behaviour of the code. Another place may have a check like $this->password === false.

The best solution would be to annotate password with string|false. And if later it turns out it can also be a resource, pointint to a private key, then we turn it into string|false|resource. Now, if it can also be a Password object, then we make it string|false|resource|Password. We must however, resist just ploping down mixed and calling it a day.

By using mixed we might as well have not added any types. Instead, we want to document our types as precise as possible, wherever we can. Now, the example of the Login class is small, but when you have a class with 200 methods, all calling each other and using its output, finding out what type something is can be difficult. You have to start with a type you are sure of. And if you can, make it an explicit type, in the language.

For example, what if we have the following method in a legacy class. When we read the code, we still don’t know what $orderData can be. Maybe the fetchOrder method will hint us to that. But when we read it we can guarantee that it returns an int or false. We can add it right away, and know we won’t change any behaviour.

class Legacy
{
  // 100 methods

  private function getOrderId($orderData)
  {
    $orderId = $this->database->fetchOrder($orderData);

    if (! $orderId) {
      return false;
    }
    
    return (int) $orderId;
  }

  // 100 more methods
}

So now we’ve added the type to our method. We may need to update the baseline of our static analysis tool, or maybe we don’t have to do any extra work. Now we can be sure that when we use the getOrderId method, we get either an integer, or false. This (hopefully) means that we can add types elsewhere.

class Legacy
{
  // 100 methods

  private function getOrderId($orderData): int|false
  {
    $orderId = $this->database->fetchOrder($orderData);

    if (! $orderId) {
      return false;
    }
    
    return (int) $orderId;
  }
  
  // 100 more methods
}

So whats the point

The first point of adding these types is to increase our understanding of what this code does. By giving it the proper types, we may detect dead code, or find bugs. If we keep up this, and the boyscout rule long enough, then it may not be legacy code anymore.

The second point is that your ‘new’ or better code probably still has to connect with legacy code. So by adding a bit more certainty, we are a bit safer that the code does what we expect it to do.

So go out there, and explore all the types your codebase has. So what if the parameter can be one of 12 interfaces, a string, or a boolean. Only once the horrors are documented do you have a chance to improve them.

Avatar
Gert de Pagter
Software Engineer

My interests include software development, math and magic.