TypeScript Section 6 – Advanced Types


TypeScript

Updated Mar 17th, 2022

Table of Contents

Chapter Details

Module Introduction

Can become handy in certain situations

Intersection Types

Allow us to combine other types

type Admin = {
  name: string
  privileges: string[]
}

type Employee = {
  name: string
  startDate: Date
}

type ElevatedEmployee = Admin & Employee

const e1: ElevatedEmployee = {
  name: "Max",
  privileges: ['create-server'],
  startDate: new Date()
}

Closely related to interface inheritance as another way to implement this code above.

Intersection types can be used with any types whereas with interface inheritance/extension only works with objects.

More on Type Guards

Used more often than “intersection types” and help with “union types.”

The concept of “type guards” is the idea or approach of checking if a certain property or method exists before you try to use it.

An example of “type guards” is when you use “typeof” in an “if check” to see the type of an incoming argument, (see if it is a string or a number, etc.) and handle differently on the result.

function add(a: Combinable, b: Combinable) {
  if (typeof a === "string || typeof b === "string") {
    return a.toSring() + b .toString()
  }
  return a + b
}

Another “type guard” is to use the “in” keyword to see if a property exists on an object.

if ("privileges" in emp) {
  console.log("Privileges " + emp.privileges)
}

When working with classes you can use the “instanceof” type guard.

class Car {
  drive() {
     console.log("Driving a car.")
  }
}

class Truck {
  drive() {
    console.log("Driving a truck.")
  }

  loadCargo(amount: number) {
    console.log("Loading cargo " + amount)
  }
}

type Vehicle = Car | Truck

const v1 = new Car()
const v2 = new Truck()

function useVehicle(vehicle: Vehicle) {
  vehicle.drive()
  if (vehicle instanceof Truck) {
    vehicle.loadCargo(1000)
  }
}

useVehicle(v1)
useVehicle(v2)

We could’ve used the “in” strategy for our “type guard” with “if (“loadCargo” in vehicle)” in the code above but we can use “typeof” as an alternative, which may be more elegant as it eliminates the risk of developer misspelling “loadCargo.”

Note: Not discussed in the course material is a typeguard check called Array.isArray(whatEvs). Per the mdn docs, this is actually preferred over instanceof because it works through iframes.

Discriminated Unions

A pattern, that is a special type of “type guard” that when working with “union types” makes working with type guards easier. Available when you work with object types. Works with classes and interfaces.

Give every interface an extra property, use any name you want.

interface Bird {
  kind: "bird"
  flyingSpeed: number
}

interface Horse {
  kind: "horse" 
  runningSpeed: number
}

type Animal = Bird | Horse

function moveAnimal(animal: Animal) {
  let speed
  switch (animal.type) {
    case "bird":
      speed: animal.flyingSpeed
      break
    case "horse":
      speed: animal.runningSpeed
      break
  } 
  console.log("Moving at speed " + speed)
}

moveAnimal(kind: bird, flyingSpeed: 10)

Note: in the example above we could’ve just named the property speed.

Note: could have used “if statement” with “in” keyword, but could end up with many “if checks.”

We have one common property in every object that makes up our union which describes that object.

Nice that we get autocompletion because TypeScript understands what’s happening. We also minimize the risk of mis-typing.

Type Casting

Help TS when it can’t decipher a type on its own. An example is something from the DOM.

Select an input html element by “id” but TS doesn’t dive into our html, so it knows its some html element but not the specific type. We can explicitly tell TS. Two syntaxes to avoid looking too similar to React’s angle bracket syntax.

Two syntaxes for TS that is equivalent. The first is the React-like syntax you may want to avoid. Pick an approach and stick with it instead of mix and match.

const userInputElement = <HTMLInputElement>document.getElementById('user-input')

Where “as” is a keyword.

const userInputElement = document.getElementById('user-input') as HTMLInputElement

An exclamation point at the end of an expression tells TS the expression in front of it will never yield null. If you are not sure you can use an “if check” to see if it is “truthy.”

if (userInputElement) {
  (userInputElement as HTMLInputElement).value = "Hi there."
}

Index Properties

Coming back to features that helps us write more flexible code. “Index types” is a feature that allow use to create objects that are more flexible regarding the properties they might hold.

interface ErrorContainer {
  // {email: "Not a valid email", username: "Must start with a character."}
  id: string
  [prop: string]: string
}

const errorBag: ErrorContainer = {
  email: 1 // throws warning
}

Writing an application where we are validating user input and have multiple user input fields and want to check certain fields and store a message to show if there is an error. Don’t know in advanced what the exact properties we will have in there. Want to use with any form in your webpage and we might have different forms, inputs, identifiers so we don’t want to limit to just “username” and “email.”

Want an object to only hold properties which store an error so we can have a “for loop” to see what is inside.

Define an “index type” with square brackets then any name of your choice for example key or prop, then a colon, then the value “type” of the property. In an object you can have strings, numbers, or symbols, but cannot use Booleans.

Don’t know property name or count but the property must be a string an its value must be a string. We can add pre-existing properties if we want as long as it matches the rules.

interface ErrorContainer {
  [prop: string]: string
}

Now we can create a constant variable named “errorBag” and set to “ErrorContainer” set equal to an object.

const errorBag = ErrorContainer = {
  email: "note a valid email."
}

Function Overloads

Feature that allows us to define multiple function signatures for one and the same function. Can have multiple possible ways of calling a function, different parameters for example.

function add(a: Combinable, b: Combinable) {
  if (typeof a === "string" || typeof b  === "string") {
    return a.toString() + b.toString()
  }
  return a + b
}

const result = add(1, 5)

// TS doesn't know a number or a string

Can’t call string methods like “.split()”

We could add “type casting” to tell TS what it gets back as a string. This is not optimal as we have to write more code. We can use function overloads to help TS infer the “type” of what is getting returned.

// repeat the function declaration

function add(a: number, b:number): number
function add(a: string, b:string): string
function add(a: Combinable, b: Combinable) {
  if (typeof a === "string" || typeof b  === "string") {
    return a.toString() + b.toString()
  }
  return a + b
}

We can say if we call the function and both arguments are a number then return a number.

So we can list some or all of the possible combinations of arguments passed.

Telling TS the different ways a function may be called.

Optional Chaining

Get data from a back-end server or database and not sure if a certain property is defined.

const fetchedDatauser = {
  id: "u1",
  name: "Max",
  job: {title: 'CEO', description: "My Own Company"}
}

console.log(fetchedUserData.job.title)

For larger applications you may not know if a property exists yet.

The vanilla JS way is to try to access “job” and if that works, we try to access “title.” This is totally valid.

console.log(fetchedUserData.job && fetchedUserData.job.title)

With TS we get a nicer way of doing this with this “optional chaining operator” with TS 3.7+ that uses a question mark. This feature was also added to JavaScript with ES2020.

console.log(fetchedUserData?.job?.title)

Safely access nested properties and data easily. Behind the scenes this is compiled to an “if check.”

Nullish Coalescing

Loosley related to optional chaining we get a feature realted to nullish data. Some input where we don’t know with certainty if it is null, undefined, or a valid piece of data

DOM API, or from back-end then we may not know, and TS will not know. If we want to store in another constant we may want to make sure if it is null we store a fallback value. We can do this with “logical or” but the problem is an “empty string” or “zero” is treated as a “falsy” value, and the default value will kick in. This may be what you want and in that case, this is a fine solution

But if you want to keep the “user input” unless it really is “null” or “undefined,” we need a different approach, and for this TS has the “??” syntax, known as the “nullish coalescing” operator.


const userInput = ""  // zero also becomes falsy and triggers default

// const storedData = userInput || 'DEFAULT'

console.log(storedData) // result is 'DEFAULT' which you may not want
const userInput = ""

const storedData = userInput ?? 'DEFAULT'

console.log(storedData) // result is an empty string
const userInput = null

const storedData = userInput ?? DEFAULT

console.log(storedData) // result is DEFAULT

Wrap Up

Useful Resources & Links