HomepageExploring TypeScript (TS 5.8 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

2 Sales pitch for TypeScript

Roughly, TypeScript is JavaScript plus type information. The latter is removed before TypeScript code is executed by JavaScript engines. Therefore, writing and deploying TypeScript is more work. Is that added work worth it? In this chapter, I’m going to argue that yes, it is. Read it if you are skeptical about TypeScript but interested in giving it a chance.

Icon “reading”You can skip this chapter if you’re already sure you want to learn and use TypeScript

2.1 Notation used in this chapter

In TypeScript code, I’ll show the errors reported by TypeScript via comments that start with @ts-expect-error – e.g.:

// @ts-expect-error: The right-hand side of an arithmetic operation
// must be of type 'any', 'number', 'bigint' or an enum type.
const value = 5 * '8';

That makes it easier to automatically test all the source code in this chapter. It’s also a built-in TypeScript feature that can be useful (albeit rarely).

2.2 TypeScript benefit: auto-completion and detecting more errors during editing

Let’s look at examples of code where TypeScript helps us – by auto-completing and by detecting errors. The first example is simple; later ones are more sophisticated.

2.2.1 Example: typos, incorrect types, missing arguments

class Point {
  x: number;
  y: number;
  constructor(x: number, y = x) {
    this.x = x;
    this.y = y;
  }
}
const point1 = new Point(3, 8);

// @ts-expect-error: Property 'z' does not exist on type 'Point'.
console.log(point1.z); // (A)

// @ts-expect-error: Property 'toUpperCase' does not exist on
// type 'number'.
point1.x.toUpperCase(); // (B)

const point2 = new Point(3); // (C)

// @ts-expect-error: Expected 1-2 arguments, but got 0.
const point3 = new Point(); // (D)

// @ts-expect-error: Argument of type 'string' is not assignable to
// parameter of type 'number'.
const point4 = new Point(3, '8'); // (E)

What is happening here?

In line A, we get auto-completion after point1. (the properties x and y of that object):

2.2.2 Example: getting function results wrong

How many issues can you see in the following JavaScript code?

function reverseString(str) {
  if (str.length === 0) {
    return str;
  }
  Array.from(str).reverse();
}

Let’s see what TypeScript tells us if we add type annotations (line A):

// @ts-expect-error: Function lacks ending return statement and
// return type does not include 'undefined'.
function reverseString(str: string): string { // (A)
  if (str.length === 0) {
    return str;
  }
  Array.from(str).reverse(); // (B)
}

TypeScript tells us:

If we fix this issue, TypeScript points out another error:

function reverseString(str: string): string { // (A)
  if (str.length === 0) {
    return str;
  }
  // @ts-expect-error: Type 'string[]' is not assignable to
  // type 'string'.
  return Array.from(str).reverse(); // (B)
}

In line B, we are returning an Array while the return type in line A says that we want to return a string. If we fix that issue too, TypeScript is finally happy with our code:

function reverseString(str: string): string {
  if (str.length === 0) {
    return str;
  }
  return Array.from(str).reverse().join('');
}

2.2.3 Example: working with optional properties

In our next example, we work with names that are defined via objects. We define the structure of those objects via the following TypeScrip type:

type NameDef = {
  name?: string, // (A)
  nick?: string, // (B)
};

In other words: NameDef objects have two properties whose values are strings. Both properties are optional – which is indicated via the question marks in line A and line B.

The following code contains an error and TypeScript warns us about it:

function getName(nameDef: NameDef): string {
  // @ts-expect-error: Type 'string | undefined' is not assignable
  // to type 'string'.
  return nameDef.nick ?? nameDef.name;
}

?? is the nullish coalescing operator that returns its left-hand side – unless it is undefined or null. In that case, it returns its right-hand side. For more information, see “Exploring JavaScript”.

nameDef.name may be missing. In that case, the result is undefined and not a string. If we fix that, TypeScript does not report any more errors:

function getName(nameDef: NameDef): string {
  return nameDef.nick ?? nameDef.name ?? '(Anonymous)';
}

2.2.4 Example: forgetting switch cases

Consider the following type for colors:

type Color = 'red' | 'green' | 'blue';

In other words: a color is either the string 'red' or the string 'green' or the string 'blue'. The following function translates such colors to CSS hexadecimal color values:

function getCssColor(color: Color): `#${string}` {
  switch (color) {
    case 'red':
      return '#FF0000';
    case 'green':
      // @ts-expect-error: Type '"00FF00"' is not assignable to
      // type '`#${string}`'.
      return '00FF00'; // (A)
    default:
      // (B)
      // @ts-expect-error: Argument of type '"blue"' is not
      // assignable to parameter of type 'never'.
      throw new UnexpectedValueError(color); // (C)
  }
}

In line A, we get an error because we return a string that is incompatible with the return type `#${string}`: It does not start with a hash symbol.

The error in line C means that we forgot a case (the value 'blue'). To understand the error message, we must know that TypeScript continually adapts the type of color:

And that type is incompatible with the special type never that the parameter of new UnexpectedValueError() has. That type is used for variables at locations that we never reach. For more information see “The bottom type never” (§15).

After we fix both errors, our code looks like this:

function getCssColor(color: Color): `#${string}` {
  switch (color) {
    case 'red':
      return '#FF0000';
    case 'green':
      return '#00FF00';
    case 'blue':
      return '#0000FF';
    default:
      throw new UnexpectedValueError(color);
  }
}

This is what the error class UnexpectedValueError looks like:

class UnexpectedValueError extends Error {
  constructor(
    // Type enables type checking
    value: never,
    // Avoid exception if `value` is:
    // - object without prototype
    // - symbol
    message = `Unexpected value: ${{}.toString.call(value)}`
  ) {
    super(message)
  }
}

Lastly, TypeScript gives us auto-completion for the argument of getCssColor() (the values 'blue', 'green' and 'red' that we can use for it):

2.2.5 Example: code handles some cases incorrectly

The following type describes content via objects. Content can be text, an image or a video:

type Content =
  | {
    kind: 'text',
    charCount: number,
  }
  | {
    kind: 'image',
    width: number,
    height: number,
  }
  | {
    kind: 'video',
    width: number,
    height: number,
    runningTimeInSeconds: number,
  }
;

In the following code, we use content incorrectly:

function getWidth(content: Content): number {
  // @ts-expect-error: Property 'width' does not exist on
  // type 'Content'.
  return content.width;
}

TypeScript warns us because not all kinds of content have the property .content. However, they all do have the property .kind – which we can use to fix the error:

function getWidth(content: Content): number {
  if (content.kind === 'text') {
    return NaN;
  }
  return content.width; // (A)
}

Note that TypeScript does not complain in line A, because we have excluded text content, which is the only content that does not have the property .width.

2.3 Type annotations for function parameters and results are good documentation

Consider the following JavaScript code:

function filter(items, callback) {
  // ···
}

That does not tell us much about the arguments expected by filter(). We also don’t know what it returns. In contrast, this is what the corresponding TypeScript code looks like:

function filter(
  items: Iterable<string>,
  callback: (item: string, index: number) => boolean
): Iterable<string> {
  // ···
}

This information tells us:

Yes, the type notation takes getting used to. But, once we understand it, we can quickly get a rough understand of what filter() does. More quickly than by reading prose in English (which, admittedly, is still needed to fill in the gaps left by the type notation and the name of the function).

I find it easier to understand TypeScript code bases than JavaScript code bases because, to me, TypeScript provides an additional layer of documentation.

This additional documentation also helps when working in teams because it is clearer how code is to be used and TypeScript often warns us if we are doing something wrong.

Whenever I migrate JavaScript code to TypeScript, I’m noticing an interesting phenomenon: In order to find the appropriate types for the parameters of a function or method, I have to check where it is invoked. That means that static types give me information locally that I otherwise have to look up elsewhere.

2.4 TypeScript benefit: better refactoring

Refactorings are automated code transformations that many integrated development environments offer.

Renaming methods is an example of a refactoring. Doing so in plain JavaScript can be tricky because the same name might refer to different methods. TypeScript has more information on how methods and types are connected, which makes renaming methods safer there.

2.5 Using TypeScript has become easier

We now often don’t need an extra build step compared to JavaScript:

More good news:

Creating packages has also improved:

Alas, type checking is still relatively slow and must be performed via the TypeScript compiler tsc.

2.6 The downsides of using TypeScript

2.7 TypeScript FAQ

2.7.1 Is TypeScript code heavyweight?

TypeScript code can be heavyweight. But it doesn’t have to be. For example, due to type inference, we can often get away with relatively few type annotations:

function setDifference<T>(set1: Set<T>, set2: Set<T>): Set<T> {
  const result = new Set<T>();
  for (const elem of set1) {
    if (!set2.has(elem)) {
      result.add(elem);
    }
  }
  return result;
}

The only non-JavaScript syntax in this code is <T>: Its first occurrence setDifference<T> means that the function setDifference() has a type parameter – a parameter at the type level. All later occurrences of <T> refer to that parameter. They mean:

Note that we normally don’t have to provide the type parameter <T> – TypeScript can extract it automatically from the types of the parameters:

assert.deepEqual(
  setDifference(new Set(['a', 'b']), new Set(['b'])),
  new Set(['a']),
);
assert.deepEqual(
  setDifference(new Set(['a', 'b']), new Set(['a', 'b'])),
  new Set(),
);

When it comes to using setDifference(), the TypeScript code is not different from JavaScript code in this case.

2.7.2 Is TypeScript trying to turn JavaScript into C# or Java?

Over time, the nature of TypeScript has evolved.

TypeScript 0.8 was released in October 2012 when JavaScript had remained stagnant for a long time. Therefore, TypeScript added features that its team felt JavaScript was missing - e.g. classes, modules and enums.

Since then, JavaScript has gained many new features. TypeScript now tracks what JavaScript provides and does not introduce new language-level features anymore – for example:

2.7.2.1 TypeScript is more than OOP

A common misconception is that TypeScript only supports a class-heavy OOP style; it supports many functional programming patterns just as well – e.g. discriminated unions which are a (slightly less elegant) version of algebraic data types:

type Content =
  | {
    kind: 'text',
    charCount: number,
  }
  | {
    kind: 'image',
    width: number,
    height: number,
  }
  | {
    kind: 'video',
    width: number,
    height: number,
    runningTimeInSeconds: number,
  }
;

In Haskell, this data type would look like this (without labels, for simplicity’s sake):

data Content =
  Text Int
  | Image Int Int
  | Video Int Int Int

More information: “TypeScript for functional programmers” in the TypeScript Handbook.

2.7.3 Advanced usage of types seems very complicated. Do I really have to learn that?

Normal use of TypeScript almost always involves relatively simple types. For libraries, complicated types can be useful but then they are complicated to write and not complicated to use. My general recommendation is to make types as simple as possible and therefore easier to understand and maintain. If types for code are too complicated then it’s often possible to simplify them – e.g. by changing the code and using two functions instead of one or by not capturing every last detail with them.

One key insight for making sense of advanced types, is that they are mostly like a new programming language at the type level and usually describe how input types are transformed into output types. In many ways, they are similar to JavaScript. There are:

For more information on this topic, see “Overview: computing with types” (§33).

2.7.3.1 Are complicated types worth it?

Sometimes they are – for example, as an experiment, I wrote a simple SQL API that gives you a lot of type completions and warnings during editing (if you make typos etc). Note that writing that API involved some work; using it is simple.

2.7.4 How long does it take to learn TypeScript?

I believe that you can learn the basics of TypeScript within a day and be productive the next day. There is still more to learn after that, but you can do so while already using it.

“The basics of TypeScript” (§4) teaches you those basics. If you are new to TypeScript, I’d love to hear from you: Is my assumption correct? Were you able to write (simple) TypeScript after reading it?