Free discovery callFree discovery call

Practical aspects of advanced TypeScript - part 1

Learn practical stuff for everyday use with Typescript

DevelopmentLast updated: 15 Feb 20245 min read

By Marko Stjepanek

If you're looking for the second part of this article, here it is:

Blog | Practical aspects of advanced TypeScript - part 2
Learn practical stuff for everyday use with Typescript

Many of us have used TypeScript, but only its basics. TypeScript is a syntactic superset of JavaScript. This means that TypeScript adds syntax on top of JavaScript so developers can add types. In order to use all that TypeScript offers, it is necessary to learn its more complex aspects. To make learning easier, the topic will be divided into several articles. This is part one.

Any vs unknown

I often see in other people’s code that they use any when they don’t know the type of their variable. We should not use any since it does not give us a lot of protection. Any is an escape hatch from the type system. Here comes unknown to the rescue. Just like all types are assignable to any, all types are assignable to unknown, but unknown isn’t assignable to anything but itself and any without a type assertion or narrowing to a more specific type with type guards.

Type guard

As said earlier type guard is a mechanism that narrows types. Usually, type guards are within a conditional block and they return a boolean value. Some ways to use a type guard are:

  • nullable type guard
  • typeof operator
  • instanceof operator
  • in operator
  • custom type guard

Nullable type guard

When we are using an optional operator we should use a nullable type guard.

function sayHello (name?: string): string {
    if(name) {
        return `Hello ${name.toUpperCase()}`
      }
    return `Hello stranger`
}

Typeof operator

typeof type guard is used to determine the type of a variable. It is limited and can determine only types:

  • boolean
  • number
  • bigint
  • string
  • symbol
  • function
  • undefined

If type is outside of this list, the typeof returns object.

function setWidth (width: string | number): void {
	if (typeof width === 'string') {
		this.width = parseInt(width)		
	} else {
		this.width = width
	}

Instanceof operator

instanceof type guard is used to check if a value is an instance of a given constructor function or a class.

class Person {
	constructor (public name: string) {}
}

function Hello (obj: unknown): void {
	if (obj instanceof Person) {
		console.log('Hello', obj.name)
	}
}

In operator

in type guard checks if an object has a particular property. It is used to differentiate between different types. The basic syntax for the in type guard is:

propertyName in objectName
"id" in { name: "Jason", id: 5 } // true
"id" in { name: "Robert" } // false
"id" in { name: "Jane", id: undefined } // true

In the example where the id property exists, the boolean true is returned. More complex example with in operator with objects will be shown in the next chapter with Union types.

Custom type guard

Typescript lets you define your own type guards. A custom type guard is a Boolean-returning function that can assert something about the type of its parameter. They are mostly used when dealing with data coming from a third party.

interface Car {
    fuel: string
    color: string
    numberOfWheels: number
}

interface Bicycle {
    color: string
    numberOfWheels: number
}

const isCar = (data: Car | Bicycle): data is Car => {
    return (data as Car).fuel !== undefined
}

function printVehicle (vehicle: Car | Bicycle): void {
    if(isCar(vehicle)) {
        console.log(`Car is ${vehicle.fuel} powered`)
    } else {
        console.log(`Bicycle is ${vehicle.color}`)
    }
}

Union and Intersection types

To get a more comprehensible and clearer type of support we use unions and intersections.

Union

It might be confusing that a union of types appears to have the intersection of those types’ properties. This is not an accident - the name union comes from the type theory. The union number | string is composed by taking the union of the values from each type. Notice that given two sets with corresponding facts about each set, only the intersection of those facts applies to the union of the sets themselves.

As we see, we only get 3 methods to use, because number and string have only these in common.

In Union, we use type guards (if statements) to narrow down the union type to its single type. Mostly we use typeof type guard.

function printId(id: number | string): void {
    if(typeof id === "number") {
        console.log(id.toString()) // only type number has toString()
    } else {
        console.log(id.slice(0, 5)) // only type string has slice()
    }
}

If we have an object we can use the in type guard. It allows us to test if a property is in an object like this:

interface Worker {
    name: string
    surname: string
    age: number
    position: string
}

interface Student {
    name: string
    surname: string
    age: number
    studentId: number
    yearOfStudy: number
}

function printProperty(user: Worker | Student): void {
    if ("studentId" in user) {
        console.log(user.yearOfStudy);
      } else {
        console.log(user.position);
    }
}

The problem with using the operator in, is when the object is complicated and we have more types and overlaps. To solve that problem, discriminated unions have shown to be practical.

Discriminated Union

In Discriminated Union we create a new property in all types that we need to check against. It scales well and works with large objects. We are going to add the property USER_TYPE to the previous example.

enum UserType {
    Worker = "Worker",
    Student = "Student"
 }

interface Worker {
    name: string
    surname: string
    age: number
    position: string
    USER_TYPE: UserType.Worker
}

interface Student {
    name: string
    surname: string
    age: number
    studentId: number
    yearOfStudy: number
    USER_TYPE: UserType.Student
}

function printProperty(user: Worker | Student): void {
    if (user.USER_TYPE === UserType.Student) {
        console.log(user.yearOfStudy);
      } else {
        console.log(user.position);
    }
}

Intersection

The intersection combines properties from one type A with another type B. The result is a new type with the properties of type A and type B. Advantage of using intersection is a more accessible update of multiple types. Also, the difference between types becomes easier to read.

// Before
type Person = {
    name: string
    surname: string
    age: number
}

type Student = {
    name: string
    surname: string
    age: number
    studentId: number
    yearOfStudy: number
}


// After
type Person = {
    name: string
    surname: string
    age: number
}

type Student = Person & {
    studentId: number
    yearOfStudy: number
}

Conclusion

With previous examples, we learned how to use unknown, type guard, union, and intersections. Stay tuned for part 2 of Practical aspects of advanced Typescript to learn more about Conditional Types, Utility Types and Generics.

Thank you for your time to read this blog! Feel free to share your thoughts about this topic and drop us an email at hello@prototyp.digital.

Related ArticlesTechnology x Design

View all articlesView all articles
( 01 )Get started

Start a Project