subscribe

Typescript is changing how I write code

Typescript is not just Javascript + types. Using TS more is slowly altering how I think about how my code should be written. My code is becoming more functional, and I’m incentivized to write things in a way that typescript is more likely to catch.

I wanted to share an isolated example of this.

In this example we need to process a chat message. This message can be either the type ‘text’, ‘picture’, or ‘video’. After this process is complete, I’m returning an ‘id’.

In the past, this is how I would have handled it:

function processMessage(message) {

  switch(message.type) {

    case 'text' :
      return processText(message); 
    case 'picture' :
      return processPicture(message);
    case 'video' :
      return processVideo(message);
    default :
      throw new Error('Unknown message type: ' + message.type);

  }

}

A direct translation to Typescript might look like this:

type Message = {
  type: 'text' | 'picture' | 'video',
  sender: string;
}


function processMessage(message: Message): number {

  switch(message.type) {

    case 'text' :
      return processText(message); 
    case 'picture' :
      return processPicture(message);
    case 'video' :
      return processVideo(message);
    default :
      throw new Error('Unknown message type: ' + message.type);

  }

}

The area of interest is the default clause. It’s perfectly reasonable in Javascript and other dynamic languages to add guards for invalid conditions.

Throwing an exception makes sense, because if an invalid type was passed, you’ll want the function to fail and not silently ignore the error case.

However, having the default clause is worse in typescript.

Typescript knows that each branch of the function will return a number, or throw an exception.

So what happens when we extend the Message type to include a new type:

type Message = {
  type: 'text' | 'picture' | 'video' | 'sticker',
  sender: string;
}

After this change, typescript will still let the processMessage function pass, and we can only find out that there was a failure case by running the code.

If we remove the default clause:

function processMessage(message: Message): number {

  switch(message.type) {

    case 'text' :
      return processText(message); 
    case 'picture' :
      return processPicture(message);
    case 'video' :
      return processVideo(message);

  }

}

Now, after adding the sticker to our type, enum, we’ll get an typescript error:

Function lacks ending return statement and return type does not include 'undefined'.(2366)

It’s better and faster to get feedback from static analysis. It also implies there’s fewer branches and therefore fewer unittests necessary. Nobody likes tests.

Another thing this example covers, is that you get more benefit from creating functions that return their results, over for example class methods that update their internal state.

If the result of an operation is emitted through a function’s return value, we can add a type for this. If the function returned nothing and had side-effects, this couldn’t have worked.

Web mentions