Skip to main content
Flo's Blog

Notes on Typescript

Typescript is a structural language

Coming from developing backends with Java, Typescript can be confusing; for example, you can have a type that can be a string or a number (denoted by |).

let anExampleVariable: string | number = "Hello World";
anExampleVariable = 22;
console.log(anExampleVariable); // prints 22

I wasn't able to imagine something like this before getting to know Typescript. But how is that possible? The answer is that Typescript is a structurally typed language and languages like Java (or C#) are nominally typed. Nominally typed means, Java cares about whether x is an instance of a class/type. On the other hand, Typescript is concerned about the shape of something (An object's shape is the names of the properties + the types of their values). This is why 22 fits into anExampleVariable.

Understanding Errors

Typescripts error messages can be overwhelming at the beginning, for example:

Argument of type '{ id: any; sent: boolean; customerId:
string; invoiceNumber: string; positions: { date: string;
hours: number; }[]; dueDate: string; invoiceDate: string; }'
 is not assignable to parameter of type 'InvoiceInput'.
Types of property 'positions' are incompatible. Type
'{ date: string; hours: number; }[]' is not assignable to type
'PositionInput[]'. Property 'description' is missing in type
'{ date: string; hours: number; }' but required in type
'PositionInput'.ts(2345)

Typically you'll start reading at the top:

Argument of type '{ id: any; sent: boolean; customerId: string;
 invoiceNumber: string; positions: { date: string;
hours: number; }[]; dueDate: string; invoiceDate: string; }'
is not assignable to parameter of type 'InvoiceInput.

Ok, that's only saying we have an error and already lots of text. The next line is more specific: Types of property 'positions' are incompatible. So there's some issue with what we have passed in with positions. The next line is basically the same information: Type '{ date: string; hours: number; }[]' is not assignable to type 'PositionInput[]'. And the last line is constructive in that case: Property 'description' is missing in type '{ date: string; hours: number; }' but required in type 'PositionInput'. ts(2345). So if the beginning of the error message doesn't give away much, scroll to the end of the message (the important info is at the bottom) - this is what you'll do most of the time when you see an error.

Now one might want to propose that error messages should place the most relevant information first and there is a discussion on Github about this.

The Typescript development lead shares insight:

"Sometimes inside-out is the best error, sometimes outside-in is the best error. It depends on whether your error is because the type "isn't even close" (i.e. you have the completely wrong kind of object), or if there's something "subtly wrong" (i.e. you are off by one property spelling or type)".

There's a distinct chapter in the typescript deep dive book that's available to read for free: https://basarat.gitbook.io/typescript/main/interpreting-errors.

You can do exhaustiveness checks via exceptions

To check for exhaustiveness means to make sure you have handled all possible cases. For example, let's say you're implementing a function that needs to work with the following type:

type Direction = "UP" | "RIGHT" | "LEFT" | "DOWN";

Then you can use typescript to determine at compile time that you have handled all possible values of Direction:

function handleInput(direction: Direction) {
  switch (direction) {
    case "DOWN":
      return "Move Down";
    case "UP":
      return "Move Up";
    case "LEFT":
      return "Move Left";
    case "RIGHT":
      return "Move Right";
    default:
      throw new UnknownDirectionError(direction);
  }
}

class UnknownDirectionError extends Error {
  constructor(value: never) {
    super("Unknown direction: " + value);
  }
}

This pattern is helpful since if you forget to handle a case, the typescript compiler will show the following error: Argument of type 'string' is not assignable to parameter of type 'never'.(2345).

You can play with the example here.

Typescript doesn’t account for undefined values when accessing array elements

// strictNullChecks is on
interface User {
  name: string;
  active: boolean;
}
const users: User[] = [{ name: "Alex", active: false }];
const activeUsers = filter(users, (user) => user.active);

console.log(activeUsers[0].name);

Does this compile? Does it run?

The answer is yes, it does compile. No, it doesn’t run! Are you surprised that this doesn’t show an error when compiling? If yes, I hope I can explain why that isn’t the case in this post.

First, let us examine the type of activeUsers[0]. When we hover over the expression within our IDE/text editor, we can see that the type is User. This, in my opinion, is unintuitive, since the list of users can be empty. Hence, at least the first value can be undefined (or any value). Moreover, if one isn’t aware of this, it will only surface as a runtime error, at worst in production. Yet the type hint doesn’t show that the actual type is the union of User and undefined (User | undefined). In case of the union, we’d have to add a type guard statement to handle the undefined value. But why isn’t that the case? Let’s think this further:

var a = [];
for (var i = 0; i < a.length; i++) {
  a[i] += 1; // tsc error: a[i] is possibly undefined
}

So we’d need to add a !, or a type guard statement:

var a = [];
for (var i = 0; i < a.length; i++) {
  if (a[i]) {
    // type guard
    a[i] += 1;
  }
}

But one could still write: (notice the <= in the first example)

const arr = [1, 2, 3];
for (let i = 0; i <= arr.length; i++) {
  console.log("Element " + i + " has value " + arr[i]!.toFixed());
}

const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
  console.log(
    "Element " +
      i +
      " has value " +
      arr[i]!.toFixed() +
      " and the next one is " +
      arr[i + 1]!.toFixed()
  );
}

As Ryan (the development lead for the TypeScript team at Microsoft) puts it:

with the ! in there the programmer thinks they've guarded against length. Then people are like "Why do I have to write ! after my array element accesses?" and the response is "So we can be sure you've thought about bounds checking", but the problem doesn't actually go away. I mean, no one just randomly accesses an element in an array without thinking about bounds. So instead you get a universe where every array element access is suffixed by ! as some meaningless ritual you have to do every time (which is annoying), but it's not actually linked to a proper bounds check in any concrete way. You know how your car beeps at you if you don't have your seatbelt on? This is useful only because it can tell if you have your seatbelt on or not. A non-useful check would be that when you started the engine, you had to press a button that said "I have my seatbelt on". You'd immediately get into the habit of pressing that button regardless of whether you were actually buckled in. That's basically safety-by-EULA. Same here.