Practical aspects of advanced TypeScript - part 1
Learn practical stuff for everyday use with Typescript
If you're looking for the second part of this article, here it is:
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.