[ui.dev] Learn Typescript - part 2

Union Types

Sometimes we need a value that could be multiple types. For example, we might have a function that measures distance in 2D space. The function accepts either a Tuple of number s or an object for the x and y coordinates.

const distance1 = measureDistance([1, 2], [1, 3]); // 1
const distance2 = measureDistance({ x: 1, y: 1 }, { x: 1, y: 3 }); // 2

The problem comes when we need to annotate our function. How do you annotate a value that could be either one type or another?

This is where Union types come in. Union types represent values that could be one of any number of types. We’ve already seen them a couple of times in the course, such as when we used optional chaining to get a value that was either a string or undefined .

For our distance formula example, we’ll put a vertical bar ( | ) between the types to create a Union type that represents both.

interface CoordinateInterface {
  x: number;
  y: number;
}
type CoordinateTuple = [number, number];

type Coordinate = CoordinateInterface | CoordinateTuple;

This reads like “The Coordinate type is either the CoordinateInterface type or the CoordinateTuple type”.

Then, in the function which calculates the distance, we have to do a bit of type narrowing to determine whether the value is a Tuple or an object. We’ll create a helper function and use Array.isArray to check if it is a Tuple.

function extractXY(point: Coordinate): CoordinateInterface {
  if (Array.isArray(point)) {
    return { x: point[0], y: point[1] };
  } else {
    return point;
  }
}

Then we can use the distance formula to calculate the distance between the points.

function measureDistance(point1: Coordinate, point2: Coordinate): number {
  const { x: x1, y: y1 } = extractXY(point1);
  const { x: x2, y: y2 } = extractXY(point2);
  // Distance Formula
  return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}

Union Types with Common Fields

When we learned about Interfaces, we extended our EdibleThing type to create Fruit and Vegetable . Lets turn that around and create a Union of Fruit and Vegetable that represents both types.

interface Fruit {
  name: string;
  sweetness: number;
}
interface Vegetable {
  name: string;
  hasSeeds: boolean;
}
type EdibleThing = Fruit | Vegetable;

Since both members of the EdibleThing union have a name:string field on them, we can access it without TypeScript complaining. However, if we try to access the sweetness or hasSeeds properties without narrowing our type to either Fruit or Vegetable , TypeScript will throw a type error.

function checkForSeeds(food: EdibleThing) {
  console.log(food.hasSeeds);
  // Type Error: Property 'hasSeeds' does not exist on type 'EdibleThing'.
  //   Property 'hasSeeds' does not exist on type 'Fruit'.
}

function checkForSeeds(food: EdibleThing) { console.log(food.hasSeeds); }

TypeScript is telling us that at least one of the members of the EdibleThing union doesn’t have the necessary property, so it throws a type error. We can’t even try to access that property without first performing some type narrowing. We can use the JavaScript in keyword to find out if an object has a particular property.

function checkForSeeds(food: EdibleThing) {
  if ("hasSeeds" in food) {
    console.log(food.hasSeeds);
  }
}

Union Types with null or undefined

One of the most valuable uses of Union types is when working with null and undefined when the TSConfig strictNullChecks flag is on. In fact, you might not have realized it, but TypeScript has already inferred a Union type for us. Any time we use an optional property, optional parameter, or optional chaining, TypeScript automatically infers that as a union with undefined . We can easily check if the value is not undefined with a truthy evaluation.

interface Fruit {
  name: string;
  sweetness?: number;
}

function getSweetness(fruit?: Fruit): number {
  const sweetness = fruit?.sweetness; // const sweetness: number | undefined
  if (sweetness) {
    return sweetness;
  }
  throw Error("'sweetness' is undefined");
}

There are a lot of other interesting things we can do with Union types. We’ll cover some of these use cases in future sections.

Intersection Types

Let’s say we have two types that are different, but similar enough that we might want to combine them together.

interface Fruit {
  name: string;
  sweetness: number;
}
interface Vegetable {
  name: string;
  hasSeeds: boolean;
}

interface EdibleThing {
  name: string;
  sweetness: number;
  hasSeeds: boolean;
}

That seems a bit redundant. What if we were able to combine these types together?

If Union types are either one type or another, Intersection types are one type and another. Intersection types combine the properties and features of two types together. Let’s go back to our Fruit and Vegetable example.

type EdibleThing = Fruit & Vegetable;

let banana: EdibleThing = { name: "Banana", sweetness: 70 };
// Type Error: Type '{ name: string; sweetness: number; }' is not assignable to type 'EdibleThing'.
//   Property 'hasSeeds' is missing in type '{ name: string; sweetness: number; }' but required in type 'Vegetable'.

let apple: EdibleThing = { name: "Apple", sweetness: 80, hasSeeds: true }; // This works

This is especially helpful when combining interfaces and other object-like types. You can’t combine everything though. Combining primitives will always yield a never type or a type that is impossible to satisfy.

type Strnumbering = string & number; // type Strnumbering = never;
type NumberAndNumberArray = number & number[];

let testNum1: number & number[] = 5; // Type Error: Type 'number' is not assignable to type 'number[]';
let testNum2: number & number[] = [5]; // Type Error: Type 'number[]' is not assignable to type 'number';

Since TypeScript also combines the types of properties with the same name, we have to be careful when combining objects with a similar shape but different types.

interface Fruit {
  name: string;
  sweetness: number;
}
interface Candy {
  name: string;
  sweetness: string;
}
type SweetThing = Fruit & Candy;

const apple: SweetThing = { name: "Apple", sweetness: 80 };
// Type Error: Type 'number' is not assignable to type 'never'.

In this case, our two sweetness properties have different primitive types, and combining them created a never type, which made it impossible to assign anything to variables of that type.

Literal Types

Literal types represent exact values of JavaScript primitives. For example, string s can represent any string, but type literalString = "thisString" can only represent a string with the value of “thisString”.

let fruitName: "Apple" = "Apple";
fruitName = "Banana"; // Type Error: Type '"Banana"' is not assignable to type '"Apple"'.

This behavior is inferred automatically when we use const to declare our variables, since that variable will never change to something different.

const fruitName = "Apple";
// const fruitName: "Apple"

A variable that can only be assigned one value isn’t very interesting. Literal types become much more powerful when we combine them with Union types. Remember when we talked about Enums, we created an object to represent our constant values.

const SEASONS = {
  winter: "winter",
  spring: "spring",
  summer: "summer",
  autumn: "autumn",
};

We then used an Enum to make this object more type safe. It isn’t very convenient, though, since we always have to reference the SEASONS object or Enum to get at the value we want. Using a Union of literal types allows us to have the same type safety as Enums, but without the extra hassle of accessing our values on the Enum itself.

type Seasons = "spring" | "summer" | "autumn" | "winter";

function seasonMessage(season: Seasons) {
  if (season === "summer") {
    return "The best season.";
  }
  return "It's alright, I guess.";
}

seasonMessage("autumn"); // It's alright, I guess.
seasonMessage("fall"); // Type Error: Argument of type '"fall"' is not assignable to parameter of type 'Seasons'.

Literal types can be created with number s and boolean s too.

type JazzBasketballRetiredJerseys = 1 | 4 | 7 | 9 | 12 | 32 | 35 | 53 | 1223;
type JazzFan = true;

We can use literal types in Interfaces and object types as well.

interface Fruit {
  foodType: "fruit";
  name: string;
  sweetness: number;
}

This pattern of putting a literal property on an interface becomes especially useful when creating Discriminating Unions, which we’ll cover in a future section.

(Practice) Union & Literal Types

Below is a CodeSandbox link which has some TypeScript code. Your job is to add all of the appropriate literal and union type annotations to the variables and values so all of the TypeScript warnings go away.

Good luck!

Union and Literal Types CodeSandbox

(Solution) Union & Literal Types

Solution

Union and Literal Types CodeSandbox Solution

(Project) Starting Template

This project uses the DOM APIs built into web browsers. If you aren’t familiar with creating web apps with DOM APIs, check out this post to get up to speed.

The starting point of this video can be found with this CodeSandbox link.

You can also find the the starting point code on Github. The commit for this video can be found here.

(Project) Adding Initial Types

The starting point of this video can be found with this CodeSandbox link.

You can also find the the starting point code on Github. The commit for this video can be found here.

(Project) Cell Event Handler

The starting point of this video can be found with this CodeSandbox link.

You can also find the the starting point code on Github. The commit for this video can be found here.

(Project) Win Condition

The starting point of this video can be found with this CodeSandbox link.

You can also find the the starting point code on Github. The commit for this video can be found here.

Class Definition

Classes are a type of object which was introduced to JavaScript in ES2015. They provide “syntactic sugar” around JavaScript’s prototypical inheritance system, making it easier to create object instances that all behave the same. This means each instance of a class has the same property structure and the same methods.

One of the reasons TypeScript was invented in the first place was to add a class syntax to JavaScript and make it more object-oriented. Now that classes are part of the JavaScript language, TypeScript fully supports them.

We define a class using the class keyword. We can then add properties to the class, along with their type definitions. Classes also need a constructor which initializes the class instance when it is created. Inside of class methods (another name for functions inside the class) we can use the this variable to access our class instance.

class Fruit {
  name: string;
  color: string;
  sweetness: number;
  constructor(name: string, color: string, sweetness: number) {
    this.name = name;
    this.color = color;
    this.sweetness = sweetness;
  }
  fullName() {
    const isSweet = this.sweetness > 50;
    return `${isSweet ? "Sweet " : ""}${this.color} ${this.name}`;
  }
}

This class has four members - three properties ( name , color , sweetness ) and one method ( fullName ).

Classes might seem very similar to Interfaces. They let you give a name to a particular shape of object and assign types to the properties of that object. However, interfaces only represent a type; classes represent both the type of a class instance and a constructor function.

When we check the typeof value of our class definition, it appears as a function . This represents the constructor, which can be called to create an instance of the class. However, we can’t just invoke the class constructor function; we have to use the new keyword so JavaScript knows to link up the class properties and methods to the class instance.

typeof Fruit; // "function"
const apple = Fruit("Apple", "red", 80); // Type Error: Value of type 'typeof Fruit' is not callable. Did you mean to include 'new'?
const banana = new Fruit("Banana", "yellow", 70); // This works

banana.fullName(); // "Sweet yellow Banana"

We can use the class type as an annotation, just like we would an Interface.

const fruitBasket: Fruit[] = [];

fruitBasket.push(new Fruit("Pear", "green", 60));

If we have the strictPropertyInitialization flag turned on in our tsconfig.json file, TypeScript will expect us to initialize all of our class properties in the constructor. If we don’t, TypeScript will throw a type error.

class Fruit {
  name: string;
  color: string;
  // Type Error: Property 'color' has no initializer and is not definitely assigned in the constructor.
  sweetness: number;
  // Type Error: Property 'sweetness' has no initializer and is not definitely assigned in the constructor.
  constructor(name: string, color?: string, sweetness?: number) {
    this.name = name;
    if (color) {
      this.color = color;
    }
    if (sweetness || sweetness === 0) {
      this.sweetness = sweetness;
    }
  }
}

There are three ways we can avoid constructor initialization. They are default values, optional properties, and the non-null assertion operator.

Default properties let us specify what the value of a property should be in case it isn’t assigned in the constructor.

class Fruit {
  name: string;
  color: string = "red";
  sweetness: number = 50;
  constructor(name: string, color?: string, sweetness?: number) {
    this.name = name;
    if (color) {
      this.color = color;
    }
    if (sweetness || sweetness === 0) {
      this.sweetness = sweetness;
    }
  }
}

Optional properties work the same in classes as they do in Interfaces. Putting ? after the property name will mark the property as optional. When we try to access these properties before they are assigned, their value will be undefined . Also, remember that optional properties only exist in TypeScript; you can’t use them when writing JavaScript.

class Fruit {
  name: string;
  color?: string;
  sweetness: number = 50;
  constructor(name: string, sweetness?: number) {
    this.name = name;
    if (sweetness || sweetness === 0) {
      this.sweetness = sweetness;
    }
  }
}

Another TypeScript-only solution is the Non-null assertion operator. It looks like an optional property, except with an exclamation mark ( ! ) instead of a question mark. Adding this bypasses the strictPropertyInitialization rule for that property, which means if we aren’t careful, the property might never be initialized and it would have the value of undefined at runtime. Only use this when you are certain you know better than the type checker.

class Fruit {
  name: string;
  sweetness!: number;
  constructor(name: string, sweetness?: number) {
    this.name = name;
    this.initSweetness(sweetness);
  }
  initSweetness(sweetness?: number) {
    if (!sweetness) {
      this.sweetness = 100;
    }
    this.sweetness = sweetness;
  }
}

Inheritance

Just like Interfaces, classes can be extended by other classes, creating an inheritance chain. This behaves just a little differently than interfaces, though, since we have to account for the constructor function. Lets create the same class inheritance that we did earlier with Interfaces and adjust it for our class.

class EdibleThing {
  name: string;
  color: string;
  constructor(name: string, color: string) {
    this.name = name;
    this.color = color;
  }
}

class Fruit extends EdibleThing {
  sweetness: number;
  constructor(name: string, color: string, sweetness: number) {
    super(name, color);
    this.sweetness = sweetness;
  }
}

super is a special function which is only available in the constructor of a class which inherits from another class. It calls the constructor of the parent class. This lets us pass the necessary parameters to the parent’s constructor so it can prepare the class instance. Because of the rules of JavaScript, we have to call super before trying to access this . Otherwise, TypeScript will give us this warning: 'super' must be called before accessing 'this' in the constructor of a derived class.

Static Properties

We can assign static properties to a class. These are properties which only exist on the class definition, not class instances. We do this by appending the static keyword in front of the property name.

class Vegetable {
  static cookingTimeSeconds = 5;
  static cook(vegetable: Vegetable) {
    setTimeout(() => {
      console.log(`Cooked ${vegetable.name}`);
    }, Vegetable.cookingTimeSeconds * 1000);
  }
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

const squash = new Vegetable("squash");
Vegetable.cook(squash); // 5 seconds later: "Cooked squash"

This is useful when we have utility functions or constants related to a specific class that we want easy access to.

Abstract Classes

Abstract classes are classes which cannot be instantiated, but provide implementation details for any classes which extend them. This differs from Interfaces, which only provide type definitions. You can think of them as being blueprints, or templates, which have to be followed when creating certain types of class definitions. They only exist in TypeScript, so you can’t use an abstract class when you are writing JavaScript.

Abstract classes can include abstract methods, which include a function type signature, but contain no implementation. Abstract methods must be implemented by the derived class; if the derived class doesn’t implement every abstract method, TypeScript will throw an error.

abstract class EdibleThing {
  name: string;
  abstract eat(): void;
  constructor(name: string) {
    this.name = name;
  }
}
class Fruit extends EdibleThing {
  constructor(name: string) {
    super(name);
  }
  eat() {
    console.log(`Yum. ${this.name}s are tasty.`);
  }
}

Modifiers

If you’ve ever written code in Java or C#, then you are probably familiar with class modifiers. These are keywords that we place before our property and method definitions which define how and when the fields on our class can be accessed. We can use modifiers to tell the TypeScript type checker how to treat our class members. The three class modifiers are public , private , and protected .

Public

By default, class fields are marked as public . This makes the fields accessible both inside and outside of the class.

class Fruit {
  name: string; // `public` modifier is implied
  public sweetness: number;
  constructor(name: string, sweetness: number) {
    this.name = name;
    this.sweetness = sweetness;
  }

  public sayName() {
    console.log(this.name);
  }
}
const apple = new Fruit("Apple", 80);
apple.name = "Banana";

apple.sayName(); // "Banana"

This is useful when we want to easily edit and modify properties or call methods outside of the class, but also gives any consumers of this class access to all of the class internals.

Protected

What if we were writing a class that will be used by other developers. Many of the parts of the class should be accessed by other developers, but other parts contain implementation details that we would prefer to keep to ourselves. We can use the protected and private modifiers to stop other developers from accessing certain properties and methods.

The protected modifier limits access to class properties and methods to the class itself and any classes that inherit the parent class. If we try to access a protected property or method, TypeScript will throw a type error.

class EdibleThing {
  protected name: string;
  constructor(name: string) {
    // We can set and modify the name from within this class
    this.name = name;
  }
}
class Fruit extends EdibleThing {
  protected sweetness: number;
  constructor(name: string, sweetness: number) {
    super(name);
    this.sweetness = sweetness;
  }
  public sayName() {
    console.log(this.name);
  }
}
const apple = new Fruit("Apple", 80);
apple.name = "Banana"; // Type Error: Property 'name' is protected and only accessible within class 'EdibleThing' and its subclasses.

apple.sayName(); // "Apple"

We can access the public sayName method, which accesses the protected name property on the parent class, but TypeScript warns us if we try to access protected name . However, if we were to ignore the warning and compile our TypeScript to JavaScript, it would still be able to access the property. TypeScript doesn’t add any runtime protections to protected class fields, so JavaScript doesn’t know that we shouldn’t be allowed to access the property.

Private

private is the most restrictive of the class modifiers. It only allows access to properties on the class they are defined on. Trying to access the field in child classes and outside the class causes TypeScript to throw a type error.

class EdibleThing {
  private name: string; // Notice the modifier has been changed
  constructor(name: string) {
    this.name = name;
  }
}
class Fruit extends EdibleThing {
  protected sweetness: number;
  constructor(name: string, sweetness: number) {
    super(name);
    this.sweetness = sweetness;
  }
  public sayName() {
    console.log(this.name); // Type Error: Property 'name' is private and only accessible within class 'EdibleThing'.
  }
}

readonly

readonly is another modifier which is not limited to classes. You can use it on Interfaces, objects, arrays, or any other type as well. Adding this modifier to a property makes it so you cannot mutate or make changes to the property whatsoever. You can think of this as the const variable declaration, but for object properties.

type readOnlyFruit = { readonly name: string };
const apple: readOnlyFruit = { name: "Apple" };
apple.name = "Banana"; // Type Error: Cannot assign to 'name' because it is a read-only property.

You can apply multiple layers of readonly modifiers to further limit what can be changed.

type fruitBasket = {
  readonly fruitList: readonly string[];
};
const basket: fruitBasket = { fruitList: ["Apple"] };
basket.fruitList[0] = "Banana"; // Type Error: Index signature in type 'readonly string[]' only permits reading.

Property Shorthand

TypeScript gives us a shorthand when our constructor parameters are the same as our properties. By assigning a modifier to constructor parameters, TypeScript will automatically assign them to the appropriate property without us even having to define the property on the class. This can save us a bit of typing.

class Fruit {
  constructor(public name: string, protected sweetness: number) {}
}
const apple = new Fruit("Apple", 80);
console.log(apple); // Fruit { name: "Apple", sweetness: 80 }

A Note about JavaScript support

Everything we’ve talked about class modifiers so far only exists in TypeScript. If you try to use these features when writing JavaScript, you will get a syntax error. Everything else in this section is a feature of JavaScript, and you can use them so long as you don’t include the class field modifiers.

Accessors (get and set)

JavaScript provides the ability to add “accessors” to classes. These define virtual properties which call functions whenever a class property is read or changed. These functions have full access to the class instance and can read, transform, and modify any other instance property. We define accessors using the get and set keyword.

Here, we’ll create a class that ensures the name is always capitalized.

class Fruit {
  constructor(protected storedName: string) {}
  set name(nameInput: string) {
    this.storedName = nameInput;
  }
  get name() {
    return this.storedName[0].toUpperCase() + this.storedName.slice(1);
  }
}

const apple = new Fruit("apple");
apple.name; // "Apple"
apple.name = "banana";
apple.name; // "Banana"

Notice that I store the name on a different property. If I were to try mutating this.name within my set name() method, it would call set name() again, creating an infinite recursive function.

Class accessors which only use get but not set are automatically marked as readonly by TypeScript.

class Fruit {
  protected storedName: string;
  constructor(name: string) {
    this.storedName = name;
  }
  get name() {
    return this.storedName[0].toUpperCase() + this.storedName.slice(1);
  }
}
const apple = new Fruit("apple");
apple.name = "banana"; // Type Error: Cannot assign to 'name' because it is a read-only property.

Adding set to classes can be helpful when we want to validate the input before assigning it to the class.

class Fruit {
  constructor(protected name: string, protected storedSweetness: number) {}
  get sweetness() {
    return this.storedSweetness;
  }
  set sweetness(sweetnessValue: number) {
    if (sweetnessValue < 0) {
      throw new Error("Sweetness cannot be less than 0");
    }
    if (sweetnessValue > 100) {
      throw new Error("Sweetness cannot be greater than 100");
    }
    this.storedSweetness = sweetnessValue;
  }
}

Here, we throw an error if the provided value is too high or too low.

ES Private Fields

ES Private Fields are a relatively new addition to the JavaScript language, and are distinctly different from TypeScript private fields. Since ES Private Fields are part of the JavaScript language, they have runtime guarantees which TypeScript cannot provide. If you mark a field as an ES Private Field, that field will not be accessible outside of the class instance.

ES Private Fields are indicated with a pound sign hashtag octothorpe number sign ( # ) immediately before the property name. They are accessed in the same way, which means you can have getters and setters that publicly expose the ES Private Field with the same name.

class Fruit {
  #name: string;
  constructor(name: string) {
    this.#name = name;
  }
  get name() {
    return this.#name[0].toUpperCase() + this.#name.slice(1);
  }
  set name(name: string) {
    this.#name = name;
  }
}
const apple = new Fruit("apple");
console.log(apple.name); // "Apple"
apple.#name = "banana"; // Type Error: Property '#name' is not accessible outside class 'Fruit' because it has a private identifier.

private or #private ?

There are a number of factors that go into whether you should use TypeScript’s private modifier or the ES Private Field for a specific property.

ES Private Fields provide “hard privacy”, which means an outside viewer can’t see those properties even if they wanted to. This stops consumers of the class from relying on internal fields, making it easier for you to change your implementation without disrupting how users consume the API. Also, classes that extend your class still won’t have access to the private fields, which can help you avoid field naming collisions.

By contrast, TypeScript’s private modifier provides “soft privacy”, allowing consumers of the class to read and edit those fields at runtime. This might be helpful for users that need to work around your API or dive in to see how the internals work.

As a rule of thumb, ES Private fields are generally what you want to use. However, if you can’t target ES Next when you compile your code, or if you can’t afford to have a lot of polyfill code, TypeScript’s private modifier is a good alternative. Also, if you need classes that extend the base class to access a private property, you should use the protected modifier.

(Practice) Classes

Below is a CodeSandbox link which has some TypeScript code. Your job is to define two classes. There are a set of tests that automatically run when you make changes to the classes. Follow the instructions in the test names to create the classes so the tests pass. Once you are done making the tests pass, make sure there are no TypeScript errors in the classes.test.ts file.

Good luck!

Classes CodeSandbox

(Solution) Classes

Solution

Classes CodeSandbox Solution

TypeScript Operators

When working with TypeScript, we deal with two kinds of things: values and types.

Values are the only thing we see in JavaScript. When working with values, we use variables to store them and assign them using a single equals ( = ). We can use operators to transform a value into some other value. Here’s a simple example.

const theAnswer: number = 15 + 27;

Here, we’re taking two number values, 15 and 27 , and adding them together with the addition ( + ) operator. We’re putting them in a variable called theAnswer which has a number type.

TypeScript types work very similarly. Types can be placed into type aliases, and we can use operators, such as the Union operator ( | ) and Intersection operator ( & ) to transform them.

type NumberOrString = number | string;

TypeScript gives us a few more operators which we can use to transform types and even derive types from values.

Type Indexed Access

Suppose you have an interface which goes a few levels deep. You want to access the type of one of those deep levels, but you don’t want to have to type it out again. And the way you are using it is a one-off, so it seems silly to create an interface just for this one type.

Using Type Indexed Access, we can grab any type from any property of any other type.

interface Fruit {
  name: string;
  color: string;
  nutrition: { name: string; amount: number }[];
}

type FruitNutritionList = Fruit["nutrition"];

We can even grab the type of individual items in an array. We can access a specific index, or we can use the number type to grab the type of every array value.

type NutritionFact = Fruit["nutrition"][0];

// Alternatively
type NutritionFact = Fruit["nutrition"][number];

typeof

Sometimes we might want to use the type of some runtime value to represent the type of another thing. This could be especially helpful when the type was inferred by TypeScript.

let rectangle = { width: 100, height: 200 };
let rectangle2: rectangle; // Type Error: 'rectangle' refers to a value, but is being used as a type here. Did you mean 'typeof rectangle'?

Uh oh. It looks like you can’t use values as types. Fortunately, the type error gives us the solution we are looking for.

Just like we can use the typeof operator in JavaScript to check the type of something at runtime, we can use the typeof operator to derive the type of a value at compile time.

let rectangle = { width: 100, height: 200 };
let rectangle2: typeof rectangle; // { width: number; height: number; }

keyof

It can also be helpful to get the different keys which are present on a type, such as when we are dynamically accessing properties. We can use the keyof operator to give us a type which represents all of these property names. In reality, it’s a union type of string literals, one for each property name.

interface Rectangle {
  width: number;
  height: number;
}
type RectangleProperties = keyof Rectangle; // type RectangleProperties = "width" | "height"

let rectangle: Rectangle = { width: 100, height: 200 };
const propertyName: RectangleProperties = "height";
console.log(rectangle[propertyName]); // 200

We can even combine keyof with typeof to get a union type with the property names of an object values.

let rectangle = { width: 100, height: 200 };
type RectangleProperties = keyof typeof rectangle; // type RectangleProperties = "width" | "height"

const assertions

TypeScript is very loose with how it interprets our literal types. For example, when we create an object literal, TypeScript infers that the types of the values are primitives, like string and number , not literal types, like "hello" and "5" .

let rectangle = { width: 100, height: 100 };
// let rectangle: {
//     width: number;
//     height: number;
// }

This is typically what we want - it would be counterproductive for every value to be immutable - but sometimes, we might want to make the properties literal. We would have to use a literal type annotation to do this, in addition to setting the value.

let rectangle: { width: 100; height: 100 } = { width: 100, height: 100 };
// let rectangle: {
//     width: 100;
//     height: 100;
// }

While that does get the job done, it’s very verbose - we’re literally repeating our object literal.

When we have any value that we are assigning that we want to be inferred as a literal type, we can use what’s called a const assertion. Remember, an assertion is a notice to the type checker that tells it more about our program so it can check our code properly. We write const assertions by putting as const after the literal value.

let rectangle = { width: 100, height: 100 } as const;
// let rectangle: {
//     readonly width: 100;
//     readonly height: 100;
// }

This has roughly the same effect as writing out the type annotation, but with much less code. It does add a readonly modifier, to indicate that these values are constant.

If we only wanted to make one of the values a literal type, we could do that in the object declaration. We can also make individual variable values literal with the same assertion.

let rectangle = { width: 100, height: 100 as const };
// let rectangle: {
//     width: number;
//     height: 100;
// }

let message = "Hello" as const;
// let message: "Hello"

This actually looks very similar to Enums. In fact, we can achieve the same level of type safety that Enums provide without all the ceremony. Just for fun, here’s an Enum, and then a similar object created with a const assertion. When we use our object in a properly annotated function, we get great type safety.

enum Seasons {
  winter,
  spring,
  summer,
  autumn,
}

const SEASONS = {
  winter: "winter",
  spring: "spring",
  summer: "summer",
  autumn: "autumn",
} as const;

function seasonsGreetings(season: typeof SEASONS[keyof typeof SEASONS]) {
  if (season === SEASONS.winter) return "⛄️";
  if (season === SEASONS.spring) return "🐰";
  if (season === SEASONS.summer) return "🏖";
  if (season === SEASONS.autumn) return "🍂";
}

Our seasonsGreetings function parameter has just as much type safety as if we had used an Enum. In fact, our fancy type signature that we used ( typeof SEASONS[keyof typeof SEASONS] ) equates to a Union of literal values, one for each value in SEASONS .

You recall that a literal array value will always inferred to be an array of whatever types we used in the literal, like so.

const assortedItems = ["hello", 5, (fruit: Fruit) => {}];
// const assortedItems: (string | number | ((fruit: Fruit) => void))[]

What a mess! Our array item type is a Union of string s, number s, and that specific function signature. This is the same problem we experienced with Tuples, where TypeScript would never infer them to be a fixed-length array of specific types; we always had to add a Tuple type annotation.

What would happen if we used a const assertion on our array literal?

const assortedItems = ["hello", 5, (fruit: Fruit) => {}] as const;
// const assortedItems: readonly ["hello", 5, (fruit: Fruit) => void]

Just adding as const to our array literal has turned it into a Tuple, without us having to add an explicit annotation. Our string and number literals stayed literal in our Tuple definition. If we were to use string and number variables, the types would be passed into the Tuple definition.

let message = "hello";
let count = 5;
const assortedItems = [message, count, (fruit: Fruit) => {}] as const;
// const assortedItems: readonly [string, number, (fruit: Fruit) => void]

Hopefully const assertions can help simplify, or even eliminate the need, for some of your type assertions.

(Bonus) Advanced Function Typing

We’re not done learning how to add types to functions.

Overloaded Functions

Sometimes you have a single function which accepts different counts of arguments or different argument types. We could easily type something like that with Union types and optional parameters. In fact, if the function is simple, it can be better to type them this way.

function stringOrArrayLength(input: string | unknown[]) {
  return input.length;
}

If we had more than a few arguments or types in our union, it could get messy. That’s where function overloading comes in.

We overload a function by adding multiple function type signatures above the function definition. If we were to overload the same function as above without using unions, it might look like this:

function stringOrArrayLength(input: string): number;
function stringOrArrayLength(input: unknown[]): number;
function stringOrArrayLength(input: { length: number }): number {
  return input.length;
}

The last function signature is known as the “implementation signature”, and has to match the signature of all of the overloads. Since both string and arrays have a length property, I can use an object with a length property, but it’s common to use any for the parameters and narrow the type of each parameter in the function implementation.

You can add as many function signatures and parameters as you want, so long as the implementation signature matches all of them.

this Parameter

When an object includes a function declaration, you can access the object with the special this variable. TypeScript provides a way for you to apply a type annotation to this so you can have type safety when accessing this .

interface IceCreamSundae {
  baseIceCream: string;
  chocolateSyrup: number;
  cherry: boolean;
}
const hotFudgeSundae = {
  baseIceCream: "vanilla",
  chocolateSyrup: 5,
  cherry: true,
  eat(this: IceCreamSundae) {
    if (this.cherry) {
      console.log("Mmmm. Tasty.");
    } else {
      console.log("Could be better.");
    }
  },
};

hotFudgeSundae.eat(); // 'Mmmm. Tasty.'

Remember, this is a special parameter to functions, so we don’t have to pass it to our eat function. When we compile this into JavaScript, the this type annotation is removed.

const hotFudgeSundae = {
  baseIceCream: "vanilla",
  chocolateSyrup: 5,
  cherry: true,
  eat() {
    if (this.cherry) {
      console.log("Mmmm. Tasty.");
    } else {
      console.log("Could be better.");
    }
  },
};

Common Type Guards

We’ve finally come to one of the most important sections in the entire course. What do you do when you have an unknown type and you want to use it in a meaningful way? What about a Union of several types? To do so safely requires us adding some runtime checks which prove to the TypeScript type checker that the value has a specific type. We call these runtime checks “type guards”.

We can create type guards using any kind of conditional - if statements, switch statements, ternaries, and a few others. We put some kind of check against the value’s type inside the if statement’s condition. Then, inside the block, our value now has the type we matched it against.

Primitive types

The most straightforward check is strict equality.

function sayAlexsNameLoud(name: unknown) {
  if (name === "Alex") {
    // name is now definitely "Alex"
    console.log(`Hey, ${name.toUpperCase()}`); // "Hey, ALEX"
  }
}

We could pass literally any value to this function because of the type guard. It’s still type safe, since TypeScript knows that inside the if block, name is a literal "Alex" type, which has the same methods and properties as a string .

That’s a little tedious if we needed to do that for literally every name. Earlier in the course, we used the typeof operator to check what primitive type a value is. We can do that with any primitive type to narrow down a value to a particular type.

function sayNameLoud(name: unknown) {
  if (typeof name === "string") {
    // name is now definitely a string
    console.log(`Hey, ${name.toUpperCase()}`);
  }
}

We could also do the inverse of this. Instead of checking to see if name is a string , we can check if it is not a string , and then return early or throw an error. Then we know that everywhere else in our function, name is a string

function sayNameLoud(name: unknown) {
  if (typeof name !== "string") return;
  // name is now definitely a string
  console.log(`Hey, ${name.toUpperCase()}`);
}

We could use switch statements to check the type as well. In this case, we’ll narrow a Union type of number and string , but we could do the same thing with unknown .

function calculateScore(score: number | string) {
  switch (typeof score) {
    case "string":
      return parseInt(score) + 10;
      break;
    case "number":
      return score + 10;
      break;
    default:
      throw new Error("Invalid type for score");
  }
}

typeof can cover us in a lot of cases, but once we get to referential types, like objects, arrays, and class instance, we can’t use it anymore. Whenever we use typeof of any of these types, it will always return “object”. We’ll have to use more sophisticated methods to determine the types of our values.

Arrays

We can check if a value is an array using the Array.isArray method.

function combineList(list: unknown): any {
  if (Array.isArray(list)) {
    list; // (parameter) list: any[]
  }
}

Our list is now a list of any s, which is better than before, but we still don’t know the type of the values inside the list. To do that, we’ll have to loop through our array to narrow the type of each item. We can use the .filter() and .map() methods on our array to do that. We could also use a for loop. Which you choose depends on your circumstances.

function combineList(list: unknown): any {
  if (Array.isArray(list)) {
    // This will filter any items which are not numbers.
    const filteredList: number[] = list.filter((item) => {
      if (typeof item !== "number") return false;
      return true;
    });

    // This will transform any items into numbers, and turn `NaN`s into 0
    const mappedList: number[] = list.map((item) => {
      const numberValue = parseFloat(item);
      if (isNaN(numberValue)) return 0;
      return numberValue;
    });

    // This does the same thing as the filter, but with a for loop
    let loopedList: number[] = [];
    for (let item of list) {
      if (typeof item == "number") {
        loopedList.push(item);
      }
    }
  }
}

Classes

We can determine whether a value is an instance of a specific class using the instanceof operator.

class Fruit {
  constructor(public name: string) {}
  eat() {
    console.log(`Mmm. ${this.name}s.`);
  }
}

function eatFruit(fruit: unknown) {
  if (fruit instanceof Fruit) {
    fruit.eat();
  }
}

Narrowing a value to a class immediately lets us know all of the properties and methods which are on the value.

Objects

Objects are a little trickier to narrow, since they could have any combination of properties and types. Most of the time we don’t care about the type of the entire object; all we want is to access one or two properties on the object.

We have already used the in operator to determine whether a property exists on an object. We can then use the typeof operator to determine that property’s type.

The in operator only works to narrow union types, so we can’t use it with unknown . Instead, we’ll have to use another special type that comes with TypeScript: object . This type represents anything that isn’t a string , number , boolean , or one of the other primitive types. Using object instead of unknown will tell TypeScript to let us attempt to access properties on this value. We can create a Union of the generic object type and an Interface with the property that we want to access.

interface Person {
  name: string;
}
function sayNameLoud(person: object | Person) {
  if ("name" in person) {
    console.log(`Hey, ${person.name.toUpperCase()}`);
  }
}

In this case, TypeScript doesn’t care what other properties are on our person type. All that matters is that it has a name property which is a string . We could actually abuse this quirk in the type checker to create code that looks type safe but throws a runtime type error.

interface Person {
  name: string;
  age: number;
}
function sayNameLoud(person: object | Person) {
  if ("age" in person) {
    console.log(`Hey, ${person.name.toUpperCase()}`);
  }
}

sayNameLoud({ age: 27 });

The best way to avoid this is to check for all the properties that we actually use so we can guarantee that they are the type we want them to be.

Hopefully this illustrates that, while TypeScript is very sophisticated and will catch 98% of your type errors, there are still ways to unintentionally get around it and write unsafe code. In the next few sections, we’ll see more ways to fool the type checker and how to avoid doing that.

Handling null and undefined

Often, we can’t know for certain whether a variable actually has a value or if it is null or undefined . For example, we can use document.getElementById() to get a DOM element with a specific ID. However, if no element exists with the ID that we provide, the function will return null . If we have the strictNullChecks flag in tsconfig.json turned on, TypeScript will warn us that the element might not exist and require us to perform a runtime check to make sure the value is present. This helps us avoid Cannot read property "x" of undefined errors.

Lets use document.getElementById to demonstrate possible ways to handle null and undefined values.

Truthy Evaluation

If we naïvely try to change the innerText of an HTML element, we will likely get a type error.

document.getElementById("messageInput").innerText = "Alex";
// Type Error: Object is possibly 'null'.

The easiest and most commonly used type guard is an if condition that checks truthiness. This means passing in the value to our conditional without doing any kind of check using strict equals ( === ). JavaScript will allow the condition to pass if the value is truthy; if the value is falsy, it will not pass. If our conditional passes, TypeScript knows that inside of our conditional, the value cannot be null or undefined .

const messageInput = document.getElementById("messageInput");
if (messageInput) {
  messageInput.innerText = "Alex";
}

Be aware that this also excludes legitimate values that are falsy, such as 0 , '' , and false . You’ll have to do additional checks if you expect your variable might be one of those falsy values.

Optional Chaining

Things can get verbose if we are trying to access properties deep within an object’s structure. We have to add several layers of if statements, or use the logical AND operator ( && ) to sort out whether the object property exists or not.

const messageInputElement = document.getElementById("messageInput");
if (messageInputElement) {
  const parentElement = messageInputElement.parentElement;
  if (parentElement) {
    const messageInputParentInnerHTML = parentElement.innerHTML;
    if (messageInputParentInnerHTML) {
      // ...
    }
  }
}

Optional Chaining is a relatively new JavaScript operator which allows you to attempt to access properties on an object whether or not they actually exist. This can be really useful when we want to access deep properties of objects that might not actually exist. We can prepend a question mark to our “dot property access” ( . ) operator to create the Optional Chaining operator ( ?. ) that lets us safely access any property, even optional or undefined properties. If the property doesn’t actually exist or is null or undefined , JavaScript will quietly return undefined for the whole expression.

const messageInputParentInnerHTML = document.getElementById("messageInput")
  ?.parentElement?.innerHTML;
messageInputParentInnerHTML; // const messageInputParentInnerHTML: string | undefined

Isn’t that nicer to read?

Our variable has a type of string | undefined because if the expression evaluates all the way to the end, innerHTML is a string; but if the expression fails anywhere along the way, either if .getElementById() returned null or if our #messageInput element doesn’t have a parent element, the entire expression will return undefined . We can then do a truthiness check to see whether messageInputParentInnerHTML is a string or not.

const messageInputParentInnerHTML = document.getElementById("messageInput")
  ?.parentElement?.innerHTML;

if (messageInputParentInnerHTML) {
  console.log(`The current contents are ${messageInputParentInnerHTML}`);
}

Also note that Optional Chaining only fails if the property is null or undefined ; falsy values like 0 and false will pass through correctly.

We can use optional chaining to optionally access items on arrays as well. We just prepend the optional chaining operator to the square brackets of our index access. In this next example, our API could correctly return a list of teachers, or it could return an error message.

function handleTeacherAPIResponse(response: {
  teachers?: string[];
  error?: string;
}) {
  const firstTeacher = response.teachers?.[0];
  firstTeacher;
}

Finally, we can optionally call methods on objects that may or may not exist. To do this, we prepend the optional chaining operator to the parentheses which call the function.

async function makeAPIRequest(url: string, log?: (message: string) => void) {
  log?.("Request started.");
  const response = await fetch(url);
  const data = await response.json();

  log?.("Request complete.");

  return data;
}

If we were to not pass a value in for the log function, the calls to that function would silently fail without throwing an error. It’s as if we wrapped the function call in a truthiness check for the function.

async function makeAPIRequest(url, log) {
  if (log) {
    log('Request started.)
  }
  // ...etc.
}

Nullish Coalescing

When you have the option, using a default value for variables that might be undefined is really helpful. Typically we use the logical OR operator ( || ) to assign the default value.

const messageInputValue =
  document.getElementById("messageInput")?.value || "Alex";
messageInputValue; // const messageInputValue: string;

The only problem with this approach is it checks against falsy values, not just null and undefined . That means when our #messageInput field is empty, it will still give us the default value since empty string is falsy.

The Nullish Coalescing operator solves this by only checking if a value is null or undefined . It works the same way as the logical OR, but with two question marks ( ?? ) instead of two vertical bars.

const messageInputValue =
  document.getElementById("messageInput")?.value ?? "Alex";
messageInputValue; // const messageInputValue: string;

Non-null Assertion

Sometimes, you just know better than the type checker. If you positively know that a particular value or property is not null or undefined , you can tell the type checker using the Non-null Assertion operator ( !. ), which looks a lot like the Optional Chaining operator. Using this operator essentially tells the type checker “There’s no way this property could possibly be null or undefined , so just pretend like it’s the correct type.”

const messageInputValue = document.getElementById("messageInput")!.value;
messageInputValue; // const messageInputValue: string;

Notice we don’t have to add a default value. TypeScript trusts that we know what we are doing and will give us the value with the appropriate type, no questions asked. Be aware that, unlike the other two operators, the Non-null assertion operator only exists in TypeScript. If you tried using it in JavaScript, it would throw a syntax error.

Remember, any time we override the default behavior of the type checker, we run the risk of introducing type errors that the type checker cannot catch for us. Using the Non-null Assertion operator removes the type safety which we wanted TypeScript to give us in the first place, so if it is at all possible, handle null and undefined properties with one of the other methods before resorting to Non-null Assertions.

(Practice) Narrowing Types

Below is a CodeSandbox link which has some TypeScript code. Your job is to narrow all of the types so the TypeScript warnings go away. Don’t change the type annotations unless the instructions tell you to - instead, use type narrowing to determine what the type actually is.

Good luck!

Narrowing Types CodeSandbox

(Solution) Narrowing Types

Solution

Narrowing Types Solution

(Bonus) Structural vs Nominal Typing

There might have been some times during the course when you noticed some interesting choices which we made as we wrote the examples. Take this example:

class Fruit {
  isFruit = true;
  constructor(public name: string) {}
}
class Apple extends Fruit {
  type: "Apple" = "Apple";
  constructor() {
    super("Apple");
  }
}
class Banana extends Fruit {
  type: "Banana" = "Banana";
  constructor() {
    super("Banana");
  }
}

We’re doing two things here which might look a little bit redundant.

  1. Our Fruit class has a property isFruit set to the literal true value. Why is this necessary when we can see the class is named Fruit ?
  2. Our Apple and Banana classes have a type property set to the literal Apple and Banana values. Why is this necessary when we can see those classes are named Apple and Banana ?

The reason for both of these extra bits of data is structural typing. This is the typing strategy which TypeScript uses to evaluate whether two types are compatible. It focuses on the shape of objects, and is often called “duck typing” (if it has a beak like a duck and waddles like a duck, it’s probably a duck.) Structural typing is a good choice for TypeScript; even though all of the type information is removed when TypeScript code is compiled into JavaScript, we can still do runtime checks against the shape of our objects.

If we didn’t have the type properties, our Apple and Banana types would be indistinguishable, since they all would share the same property: name:string . Adding those extra properties helps us distinguish between them, both in the type system and at runtime.

One other thing to note is that different ways of describing structured types, such as classes, interfaces, and literal object types, are all structurally equivalent as well. All three of these types are equivalent in TypeScript.

class AppleClass {
  type: "Apple";
  name: string;
}
interface AppleInterface {
  type: "Apple";
  name: string;
}
type AppleType = {
  type: "Apple";
  name: string;
};

Other programming languages, like Java and C# use a nominal type system. The word “nominal” refers to the name of the thing, which means if I were to create two classes with an identical structure, but different names, those two classes would be considered different.

Like we saw with the Apple and Banana classes, we can emulate nominal typing by adding a unique property to our types with a string literal type. This practice is called “branding”, or “tagging”, and is what allowed us to differentiate between the two types in the type system.

When using a branded type, you want to make sure you assign your variables with the correct type; otherwise TypeScript will infer the type based on what value you put in your variable.

let banana = new Apple(); // const banana: Apple
let apple: Apple = new Banana(); // Type 'Banana' is not assignable to type 'Apple'.

This makes it possible to discriminate between types that may have the same structure, but different purposes.

We can also create what are called “branded primitives”. This allows us to create primitive values which are only assignable to variables and parameters that match a specific type signature.

We first create our branded primitive type. In this case, I’ll make a number that represents money from different countries.

type USD = number & { _brand: "USD" };
type EUR = number & { _brand: "EUR" };

Then, we can assign it to variables. If we just try assigning numbers to variables annotated with our branded types, TypeScript will give a warning.

let income: USD = 10; // Type Error: Type 'number' is not assignable to type 'USD'.

Instead, we have to use an assertion signature to convert our number type into the branded type. Then, we won’t be able to assign USD numbers to EUR variables without converting the type (and value if necessary) first.

let VAT = 10 as EUR;

function convertToUSD(input: EUR): USD {
  return (input * 1.18) as USD;
}

let VATInUSD = convertToUSD(VAT); // let VATInUSD = USD;

Now, not only are our objects type safe, but our units are type safe too!

Discriminating Unions

Sometimes we might have two interfaces that are very similar in shape but with key differences. If we had a Union of those two interfaces, we could access any of the common properties without a problem. However, if we were to try accessing one of the properties that only exists on one of the Interfaces, TypeScript would warn us.

interface Fruit {
  name: string;
  color: string;
  juice: () => void;
}

interface Vegetable {
  name: string;
  color: string;
  steam: () => void;
}

type EdibleThing = Fruit | Vegetable;

function prepareEdibleThing(food: EdibleThing) {
  food.juice();
  // Type Error: Property 'juice' does not exist on type 'EdibleThing'.
  //    Property 'juice' does not exist on type 'Vegetable'.
}

TypeScript is telling us that juicing vegetables is gross a Vegetable doesn’t have the property we’re looking for, and since the parameter we pass to the function could be a Vegetable , we can’t access the property without doing more checks.

Using Optional Chaining doesn’t help, since food is already guaranteed to not be null or undefined . One thing that does work is using the in keyword.

function prepareEdibleThing(food: EdibleThing) {
  if ("juice" in food) {
    food.juice();
  }
}

This works well when we only need to access a single property, but what if we needed to access many properties? Or what if we had more than two types in the Union?

This is where Discriminating Unions come into play. We can create a Discriminating Union by adding literal type properties to the interfaces which are part of the union. We can then check that individual property to see what type represents the value.

interface Fruit {
  type: "fruit";
  name: string;
  color: string;
  juice: () => void;
}

interface Vegetable {
  type: "vegetable";
  name: string;
  color: string;
  steam: () => void;
}

type EdibleThing = Fruit | Vegetable;

function prepareEdibleThing(food: EdibleThing) {
  if (food.type === "fruit") {
    food.juice();
  }
  if (food.type === "vegetable") {
    food.steam();
  }
}

This only works because all of the members of the Union have a similar property, but the property doesn’t have to be a literal type. As long as the type is different for each of the members, we can check against it. For example, TypeScript’s type checker is intelligent enough to determine the type when we use a truthiness check on a member that could be null.

type StringResult =
  | { error: Error; data: null }
  | { error: null; data: string };

function handleResult(result: StringResult) {
  if (result.error) {
    // Handle the error, which we know is an Error type
  }

  return result.data;
}

(Practice) Discriminating Unions

Below is a CodeSandbox link which has some TypeScript code. Your job is to add the appropriate properties and checks so all of the TypeScript warnings go away.

Good luck!

Discriminating Unions CodeSandbox

(Solution) Discriminating Unions

Solution

Discriminating Unions CodeSandbox Solution

Assertion Signatures

We’ll start this section off with an example. Lets suppose we’re trying to create a dynamically generated graphic using the Canvas API. We already have a <canvas> element in the HTML of the page, so we can just access it using our DOM APIs. Once we have reference to the element, we can create a 2D drawing context, and then start making graphics.

TypeScript knows the types which are returned by document.getElementById and canvas.getContext , so we should be able to use them without applying any type annotations.

const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d"); // Type Error: Property 'getContext' does not exist on type 'HTMLElement'.

Oh, that’s a bummer. It looks like document.getElementById returns an HTMLElement instead of the HTMLCanvasElement that we need, even though we know the element is most certainly a <canvas> element. Well, let’s try adding a type annotation to our canvas variable.

const canvas: HTMLCanvasElement = document.getElementById("canvas");
// Type Error: Type 'HTMLElement' is missing the following properties from type 'HTMLCanvasElement': height, width, ...

Oh no! The HTMLElement type which is returned by getElementById is not assignable to a variable with the type HTMLCanvasElement . That’s because HTMLElement doesn’t have the necessary properties defined to make it compatible with HTMLCanvasElement .

But we know that it is in fact a <canvas> element! We just need a way to tell TypeScript that.

Assertion Signatures

To assert to the type checker that a value has a specific type, we just append the keyword as , followed by the type we want to assert. This tells the TypeScript type checker that a certain value is in fact the type we say it is.

const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const context = canvas.getContext("2d");

TypeScript trusts us to know what we’re doing, and now lets us access the properties on HTMLCanvasElement .

Once again, this is a situation where we know more than TypeScript and get to override the behavior of the type checker. TypeScript isn’t doing any runtime checks for us to make sure that canvas really is a HTMLCanvasElement . Any time we do this, we run the risk of being wrong and creating type errors which the type checker can’t catch for us.

Still, Assertion signatures are safer than most methods, since the type checker will verify that the type we are asserting is at least similar to the original type. That keeps us from asserting that one type is a totally incompatible type.

let fruitName: number = "banana" as number;
// Type Error: Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other.

The type checker is even smart enough to avoid conversions between interfaces that aren’t similar enough.

interface Car {
  make: string;
  model: string;
  color: string;
}
interface Fruit {
  name: string;
  color: string;
  sweetness: number;
}

let car: Car = { make: "Pontiac", model: "Sunfire", color: "silver" };
let fruityCar: Fruit = car as Fruit;
// Type Error: Conversion of type 'Car' to type 'Fruit' may be a mistake because neither type sufficiently overlaps with the other.
//   Type 'Car' is missing the following properties from type 'Fruit': name, sweetness

Double Assertion Signatures

We’re about to dive into something that is exceptionally dangerous. Only do this if you are confident you won’t introduce any type errors.

We can convince TypeScript that any value of any type has any other type. It all starts by giving a value an assertion that it is unknown . Any value can be converted into unknown , so this isn’t very strange so far.

const ageInYears = "too old to count" as unknown;
ageInYears; // const ageInYears: unknown

But now, what if we were to add another assertion, turning that unknown type into a different type?

const ageInYears = ("too old to count" as unknown) as number;
ageInYears; // const ageInYears: number

We can even try to access number properties on our value. This, of course, will throw a type error.

ageInYears.toFixed(8); // Uncaught TypeError: ageInYears.toFixed is not a function

This is the most dangerous way to get around the type system, even more dangerous than any . Not only does it open you up to unexpected type errors, but TypeScript can’t even warn you that they might happen. You’ve convinced it everything is fine.

This can be helpful when you are certain something should be a certain type and TypeScript isn’t letting you convert with a single assertion signature. This is especially useful when you are working with interfaces or third-party APIs which expect parameters to be passed as a certain type.

We should only convert a value’s type to unknown if there is no other solution. It’s much safer for us to convert to a type that is common between the two different types. In this next example, I have a reusable buttonEventListener function which only accepts HTMLButtonElement values. However, I can use it with an HTMLAnchorElement value by first converting my anchor value into the common HTMLElement type, and then converting that into an HTMLButtonElement .

HTMLElement is similar enough to both types that we can use it without TypeScript warning us, and our code will be more type safe than if we converted our type to unknown , since at very least our value has to match the HTMLElement type.

function buttonEventListener(
  event: string,
  listener: any,
  element: HTMLButtonElement
) {
  element.addEventListener(event, listener);
}

const anchor = document.createElement("a");
buttonEventListener("click", () => console.log("Mouse clicked"), anchor);
// Type Error: Argument of type 'HTMLAnchorElement' is not assignable to parameter of type 'HTMLButtonElement'.

buttonEventListener(
  "click",
  () => console.log("Mouse moved"),
  (anchor as HTMLElement) as HTMLButtonElement
);
// no error

In this example, our buttonEventListener function could be more expressive or allow more types to be used for element , but our solution offers a good workaround.

User Defined Type Guards

This whole section has been about the type guards which we can use to narrow the type of a value to be more specific. Examples include typeof , instanceof , in , and Array.isArray . Each of these is able to assert to TypeScript that a value either does or does not conform to some type.

We can define our own user-defined type guards too. These can be especially helpful when checking the type of a value is a complicated process, or if we want to reuse the same runtime checking logic in multiple places.

Suppose we have two interfaces. We can’t modify these interfaces for some reason - perhaps they are part of a third-party library - so we can’t take advantage of Discriminating Unions. However, we still need some way to tell the difference between them.

interface Fruit {
  name: string;
  color: string;
  sweetness: number;
}
interface Vegetable {
  name: string;
  color: string;
  tenderness: number;
}

A user defined type guard is a function that takes at least one argument, returns a boolean, and has a type predicate return signature. This is a special type signature which says “this value is most certainly this type”.

function isFruit(maybeFruit: Fruit | Vegetable): maybeFruit is Fruit {
  if ("sweetness" in maybeFruit) return true;
  return false;
}

const tomato = { name: "Tomato", color: "red", tenderness: 70 };
if (isFruit(tomato)) {
  console.log(`Tomato is ${tomato.sweetness}% sweet.`);
} else {
  console.log(`Tomato is ${tomato.tenderness}% tender.`);
}

// "Tomato is 70% tender."

Our type predicate, maybeFruit is Fruit , tells us that if this function returns true, then maybeFruit is definitely a Fruit . Since tomato doesn’t have a sweetness property, it is recognized as a Vegetable , which lets us access the tenderness property without warning.

We can define type guards on unknown and any types too, allowing us to easily narrow them to any type we like.

function isFruit(maybeFruit: any): maybeFruit is Fruit {
  if ("color" in maybeFruit) return true;
  return false;
}

While this would work some of the time, other times it might give us the wrong type. If we were to pass a Vegetable to this function, it would transform its type into a fruit. What if the color property were present on maybeFruit , but it was actually a number instead of a string?

If we wanted to, we could use user defined type guard functions to trick the TypeScript compiler that any value is any other type. Obviously, that would cause type errors in our program that TypeScript couldn’t warn us about, so we should be as thorough as possible when writing the conditions in user defined type guards, especially if the type predicate is changing an any or unknown type to another type.

Assertion Functions

Assertion functions are another kind of type guard that use a different method to tell the type checker what type a value has. Assertion functions allow you to throw errors to assert a type condition. TypeScript isn’t sophisticated enough to know whether a function that throws an error asserts any kind of types on your values. We have to add an assertion signature to our assertion function to let TypeScript know that throwing an error proves something about a type.

There are two kinds of assertion signatures. The first type asserts that a boolean argument is true. We have to pass in a argument, and then we can add asserts <parameter name> as our function return signature. If the function doesn’t throw an error, it influences the type of the values used in the condition.

function assertTrue(condition: boolean): asserts condition {
  if (!condition) {
    throw new Error();
  }
}

const maybeFruitName: unknown = "Banana";

assertTrue(typeof maybeFruitName === "string");

maybeFruitName; // const maybeFruitName: string;

This works in a similar way to using if statements to return early, but it throws an error instead.

The second assertion signature is written more like a type predicate. It allows us to assert that if the function does not throw an error, a function argument is a specific type.

function assertIsFruit(maybeFruit: any): asserts maybeFruit is Fruit {
  if (!("sweetness" in maybeFruit)) throw new Error();
}

const tomato = { name: "Tomato", color: "red", tenderness: 70 };
assertIsFruit(tomato);

tomato; // const tomato: Fruit

Both Assertion Functions and Type Predicates allow us to write functions which assert or prove something about the types of the values which are passed into them, giving us more flexibility with how we perform runtime type checks of our values.

(Practice) User Defined Type Guards

Below is a CodeSandbox link which has some TypeScript code. Your job is to implement some user defined type guards with type predicates and assertion functions so all of the TypeScript warnings go away.

Good luck!

User Defined Type Guards CodeSandbox

(Solution) User Defined Type Guards

Solution

User Defined Type Guards CodeSandbox Solution