Categories
JavaScript TypeScript

TypeScript Data Types – Numbers, Strings, and Objects

JavaScript, like any other programming language, has its own data structures and types. JavaScript has a few data types that we have to know about in order to build programs with it. Different pieces of data can be put together to build more complex data structures.

JavaScript is a loosely typed, or dynamically typed, language. This means that a variable that’s declared with one type can be converted to another type without explicitly converting the data to another type. Variables can also contain any type at any time, depending on what’s assigned.

With dynamically typed languages, it’s hard to determine the type that a variable has without logging it, and we might assign data that we don’t want in the variable.

TypeScript rectifies these issues by letting us set fixed types for variables so that we’re sure of the types. It has all the basic data types of JavaScript plus the types that are exclusive to TypeScript, like numbers, strings, and objects.


Numbers

There are two number types in JavaScript, which are number and BigInt. The number type is a double-precision 64-bit number that can have values between -2 to the 53rd power minus 1 and 2 to the 53rd power minus 1. There’s no specific type for integers. All numbers are floating-point numbers. There are also three symbolic values: Infinity, -Infinity, and NaN.

The largest and smallest available values for a number are Infinity and -Infinity, respectively. We can also use the constants Number.MAX_VALUE or Number.MIN_VALUE to represent the largest and smallest numbers. We can use the Number.isSafeInteger() function to check whether a number is in the range of numbers available that are allowed in JavaScript.

There are also the constants Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_NUMBER to check if the number you specify is in the safe range. Anything outside the range isn’t safe and will be a double-precision floating-point of the value. The number 0 has two representations in JavaScript: There’s +0 and -0, and 0 is an alias for +0. It will be noticed if you try to divide a number by 0:

1/+0 // Infinity  
1/-0 // -Infinity

Sometimes numbers can represent Boolean values with bitwise operators to operate them as Boolean, but this is bad practice since JavaScript already has Boolean types, so using numbers to represent Boolean will be unclear to people reading the code. That’s because numbers can represent numbers, or they can represent Booleans if someone chooses to use them that way.

We can declare numbers like in the following code with TypeScript:

const x: number = 1  
const y: number = x + 1;

In TypeScript, there is the BigInt type to store numbers that are beyond the safe integer range. A BigInt number can be created by adding an n character to the end of a number. With BigInt, we can make calculations that have results beyond the safe range of normal numbers. For example, we can write the following expressions and still get the numbers we expect:

const x: bigint = 2n ** 55n;  
const y: bigint = x + 1n;

For x we get 36028797018963968n and for y we get 36028797018963969n, which is what we expect. BigInts can use the same arithmetic operations as numbers, such as +, *, -, ** and %. A BigInt behaves like a number when converted to a Boolean, with functions, keywords, or operators like Boolean, if , || , &&, !. BigInts cannot be operated in the same expressions as numbers. If we try that, we will get a TypeError. This is enforced with TypeScript compiler checks before compilation is done.


Strings

Strings are used to represent textual data. Each element of the string has its own position in the string. It’s zero-indexed, so the position of the first character of a string is 0. The length property of the string has the total number of characters of the string.

JavaScript strings are immutable. We cannot modify a string that has been created, but we can still create a new string that contains the originally defined string. We can extract substrings from a string with the substr() function and use the concat() function to concatenate two strings.

We should only present text data with strings. If there are more complex structures needed for your data structure, then they shouldn’t be represented with a string. Instead, they should be objects. This is because it’s easy to make mistakes with strings since we can put in the characters we want.

We can declare strings in TypeScript with the following code:

const x: string = 'abc';

Objects

Object is a reference data type, which means it can be referenced by an identifier that points to the location of the object in memory. In memory, the object’s value is stored, and, with the identifier, we can access the value. Object has properties, which are key-value pairs with the values being able to contain the data with primitive types or other objects.

That means we can use object to build complex data structures. The key is an identifier for the values of a property, which can be stored as a string or symbol. There are two types of properties that have certain attributes in an object. Objects have data properties and accessor properties.

A JavaScript object has the following data properties:

  • [[Value]] — This can be of any type. It has the value retrieved by a getter of the property. Defaults to undefined.
  • [[Writable]] — This is a Boolean value. If it’s false, then [[Value]] can’t be changed. Defaults to false.
  • [[Enumerable]] — This is a Boolean value. If it’s true, then it can be iterated over by the for...in loop, which is used to iterate over the properties of an object. Defaults to false.
  • [[Configurable]] — This is a Boolean value. If it’s true, then the property can be deleted or changed to an accessor property, and all attributes can be changed. Otherwise, the property can’t be deleted or changed to an accessor property, and attributes other than [[Value]] and [[Writable]] can’t be changed. Defaults to false.

A JavaScript object has the following accessor properties:

  • [[Get]] — This is either a function or it’s undefined. This may contain a function that is used to retrieve a property value whenever a property is being retrieved. Defaults to undefined.
  • [[Set]] — This is either a function or it’s undefined . This lets us set the assigned value to an object’s property whenever an object’s property is attempted to be changed. Defaults to undefined.
  • [[Enumerable]] — This is a Boolean value that defaults to false. If it’s true, then the property will be included when we loop through the properties with the for...in loop.
  • [[Configurable]] — This is a Boolean value that defaults to false. If it’s false, then we can’t delete the property and can’t make changes to it.

JavaScript has a Date object built into the standard library, so we can use it to manipulate dates.

Array are also objects. Array can store a list of data with integer indexes to indicate its position. The first index of JavaScript arrays is 0. There’s also a length property to get the size of the array. The Array object has many convenient methods to manipulate arrays, like the push method to add items to the end of the array and the indexOf method to find the index of the first occurrence of a given value. ATypedArray object is an object that lets us see an array-like view of a binary data buffer.

Since ES2015, the following typed array objects are available:

  • Int8Array, value ranges from -128 to 127
  • Uint8Array, value ranges from 0 to 255
  • Uint8ClampedArray, value ranges from 0 to 255
  • Int16Array, value ranges from -32768 to 32767
  • Uint16Array, value ranges from 0 to 65535
  • Int32Array, value ranges from -2147483648 to 2147483647
  • Unit32Array, value ranges from 0 to 4294967295
  • Floar32Array, value ranges from -1.2 times 10 to the 38 to 3.4times 10 to the 38
  • Float64Array, value ranges from 5.0 times 10 to the 324 to 1.8 times 10 to the 308
  • BigInt64Array, value ranges from -2 to the 63 to 2 to the 63 minus 1
  • BigUint64Array, value ranges from 0 to 2 to the 64 minus 1

Since ES2015, we have new iterable object types. They are Map, Set, WeakMap, andWeakSet. Set and WeakSet represent sets of objects, and Map and WeakMap represent objects with a list of key-value pairs. Map keys can be iterated over, butWeakMap’s keys cannot be.

With TypeScript, we put constructor names after the colon in the variable declaration to declare their types. For example, if we want to declare a Map object, we can write:

const map: Map<string, number> = new Map([['a', 1], ['b', 2]]);

Note that the way to declare some objects like Maps is different from JavaScript. This is because TypeScript supports generics. Generics in TypeScript allows us to pass in an indeterminate type to functions, interfaces, and classes (which are just syntactic sugar for functions) so that the actual type can be passed as we reference the object in the code, as we did above. The declaration above is type-safe, unlike the JavaScript way to declare Map objects. With the code above, the keys of the Map are always strings and the values are always numbers.

With dynamically typed languages, it’s hard to determine the type that a variable has without logging it, and we might assign data that we don’t want in the variable like JavaScript. TypeScript rectifies these issues by letting us set fixed types for variables so that we’re sure of the types. It has all the basic data types of JavaScript plus the types that are exclusive to TypeScript. We’ll go into more details in other TypeScript articles, where we will explore TypeScript-only data types, interfaces, combining multiple types, and more.

Categories
JavaScript TypeScript

Introduction to TypeScript Functions — This Object and Overloads

Functions are small blocks of code that take an input and either return an output or have side effects, which means that if modifies variables outside the function. We need functions to organize code into small blocks that are reusable. Without functions, if we want to re-run a piece of code, we have to copy it to different places. Functions are critical to any TypeScript program. In this part of the series, we continue to look at different parts of TypeScript functions, including how to deal with the this object with TypeScript, and overloading functions.


This Object

If this is referenced in the regular function declared with the function keyword, the this object isn’t set inside an arrow function to the function that has the this inside. If an arrow function is inside a constructor, then this is set to the new object. If it’s not inside an object, then this inside the arrow function is undefined in strict mode. this will be set to the base object if the arrow function is inside an object. However, we always get the window object if we reference this in an arrow function. For example, the following code wouldn’t compile and run since TypeScript doesn’t allow our code to have the global variable as the value for this:

const fn = () => this  
console.log(fn());

We get the window object logged when console.log is run. Likewise, if we ran this:

let obj = {}  
obj.f = () => {  
  return this;  
};  
console.log(obj.f());

We get the same thing as we got before. This is in contrast to the traditional functions declared with the function keyword. Let’s see what happens if we replace the functions above with traditional functions in the code above, as in the following code:

const fn = function() {  
  return this  
};  
console.log(fn);

Now, if we have noImplicitThis flag on in our tsconfig.json, we get the following error from the TypeScript compiler: 'this' implicitly has type ‘any’ because it does not have a type annotation.(2683)

We’re closer to getting our code working, but it’s still not going to compile. To fix this error, we need to put a fake parameter, this: void, in the function signature, as in the code below:

const fn = function(this: void) {  
    return this;  
};  
console.log(fn);

With the code above, we make the this variable unusable. If we want to use it for something, we can add an explicit type for this instead of void, to make it do something. For example, if we want to make a constructor object, we can write something like the following code:

In the code above, we created an interface called Person to add a data type to the this object in the greet function in the person object. When we call the greet function, the this parameter is ignored since we placed it as the first parameter. TypeScript only looks at the type of this in the parameter and will not expect us to call the greet function by passing in an argument for this. After defining the person object, we can then assign values to the name and age properties outside it. We already met the requirements listed in the interface when we’re defining the person object, but we should also assign some meaning value to it so we can use the greet function.

Then, when we run we run the console.log in the last line of the example above, we get Hi Jane. You’re 20 years old. We’ve successfully created a data type for this, so there won’t be any ambiguity as to what the value of this is. This helps developers understand what this has in the code since this is one of the more confusing aspects of JavaScript. In plain JavaScript, this can take on different values depending on where the this keyword is located in the code. In traditional functions, this’s value would be the function itself. Arrow functions do not change the value of this, so whatever it was outside is the same as whatever it is inside the arrow function.

With TypeScript, we can’t use the this and traditional functions to create classes. For example, if we write:

Then we would get the error Cannot find name ‘Person’. Did you mean ‘person’?(2552) and the code won’t compile. TypeScript doesn’t let us use traditional functions as classes. To use make classes, we must use the class syntax.


‘this’ Parameters in Callbacks

For callback functions used for event listeners, the callback functions that we pass in should be typed in an interface. For example, for setting the type of custom input control components, we can write something like Tthis:

interface InputElement {  
  addKeyUpListener(onclick: (this: void, e: Event) => void): void;  
}

Then, when people that use the control, then they can write something like this for it to run:

What if developers that use the library try to reference this, as in the following code?:

In this case, the TypeScript compiler will throw an error since this is marked with the void type in the InputElement interface.


Function Overloads

Overloading a function is creating functions with the same name, but with different signatures. This isn’t allowed in JavaScript since functions are objects and we can’t re-declare the same object multiple times. However, since TypeScript is strongly typed, which is the opposite of the dynamic nature of JavaScript, it has to find a way to accommodate the dynamic aspects of JavaScript. To do this, TypeScript provides us a way to overload functions with different signatures. To overload functions with TypeScript, we just have to write multiple function signatures with the same name before defining the actual function with that name. For example, we can write something like this:

function getPerson(person: { name: string, age: number }): { name: string, age: number };  
function getPerson(person: { name: string }): { name: string };  
function getPerson(person: any): any {  
  return person;  
}

With the code above, we have a getPerson function that can either accept an object with the properties name and age , or just the property name. The return type, which is after the colon, can either be an object with both the name and age properties, or an object with just the name property. This is what we have in the first three lines of the code example above, where we just define the signatures we want for our getPerson function.

Then in the actual getPerson function definition, we have the actual function definition. We annotate the type of the parameter and return as any , which is OK because we already defined the parameters and return types that the getPerson accepts and returns respectively. We can call the getPerson, as in the code below:

console.log(getPerson({ name: 'Jane', age: 20 }));  
console.log(getPerson({ name: 'Jane' }));

From the console.log statements above, we get:

{name: "Jane", age: 20}  
{name: "Jane"}

If we try to call it with an argument that we didn’t define in the signature like: console.log(getPerson({})) we get a No overload matches this call error.

The main way to deal with the this object in traditional functions is to pass it in as the first parameter of a function and then set a type to it by defining an interface for the type. TypeScript will ignore the this parameter and treat it as if it doesn’t exist. It’s only used for setting the data type of this. We can overload functions in TypeScript, which isn’t allowed in JavaScript. To do this, we just define different signatures for the function and give them the same name, then define the function with the same name after we defined the signatures that our function will accept.

Categories
JavaScript TypeScript

Introduction to TypeScript Classes — Static Properties, Abstract Classes and More

Classes in TypeScript, as in JavaScript, are a special syntax for its prototypical inheritance model that’s a comparable inheritance in class-based object oriented languages. Classes are just special functions added to ES6 that are meant to mimic the class keyword from these other languages.

In JavaScript, we can have class declarations and class expressions because they’re just functions. So like all other functions, there are function declarations and function expressions. This is the same with TypeScript. Classes serve as templates to create new objects. TypeScript extends the syntax of classes of JavaScript and then adds its own twist to it. In this piece, we’ll look at static properties, abstract classes, and constructor functions.


Static Properties

With TypeScript, we can designate members as instance variables, which don’t have the keyword static before them, and static members, which have the keyword static keyword before them.

Static members can be accessed without having the class instantiated. Of course, this is given to the access modifiers that are designated for the member. So public static members can be accessed directly from outside the class. Private static members can only be used within the class, and protected members can be accessed by a class the member is defined in and also by subclasses of that class.

The static keyword can be used by both fields and methods. For example, we can use it like in the following code:

class Person {  
  static numPeople = 0;    
  constructor(name: string) {  
    Person.numPeople++;  
  }  
  static getNumPeople() {  
    return this.numPeople;  
  }  
}

const john = new Person('John');  
const jane = new Person('Jane');  
console.log(Person.numPeople);  
console.log(Person.getNumPeople());

In the code above, every time we instantiate the Person class, we increase the static property numPeople by one. Since static properties are shared by all instances of the class and don’t belong to any one instance, we can access numPeople directly by using Person.numPeople.

Likewise, we have a static method, getNumPeople, we can call directly without instantiating the Person class. Therefore, when we get the numPeople by using Person.numPeople and call the Person.getNumPeople(), then the value 2 is returned for both.

Since the members are static, the values is part of a class and not part of any instance, so even if the instances are destroyed the values will be kept. This is different from instance variables, which are accessed from the this inside the class and the variable with the instance of the class outside the class.


Abstract Classes

TypeScript has abstract classes, which are classes that have partial implementation of a class and in which other classes can be derived from. They can’t be instantiated directly.

Unlike interfaces, abstract classes can have implementation details for their members. To declare an abstract class, we can use the abstract keyword. We can also use the abstract keyword for methods to declare abstract methods, which are implemented by classes that derive from an abstract class.

Abstract methods don’t contain implementations of the method. It’s up to the subclasses that derive from the abstract class to implement the method listed. They may also, optionally, include access modifiers.

We can use abstract classes and methods as demonstrated in the following code:

abstract class Person {  
  name: string;  
  age: number;  
  constructor(name: string, age: number) {  
    this.name = name;  
    this.age = age;  
  } abstract getName(): string;  
  abstract getAge(): number;  
}

class Employee extends Person{  
  constructor(name: string, age: number) {  
    super(name, age);  
  } 

  getName() {  
    return this.name;  
  } 

  getAge() {  
    return this.age;  
  }  
}

let employee = new Employee('Jane', 20);  
console.log(employee.getName());  
console.log(employee.getAge());

In the code above, we have the abstract Person class, which has the abstract methods getName and getAge.

As we can see, the abstract methods only have signatures in them. The actual implementation of the methods are in the Employee class, which extends the Person class.

We have the actual implementation of the getName and getAge methods in the Employee class.

TypeScript checks the method signature and the return type of the class, so we must be implementing the abstract methods as it’s outlined in the abstract class. This means that in the example above, the getName method must take no parameters and must return a string.

Likewise, the getAge method must take no parameters and must return a number. After the abstract methods have been implemented, we can call them normally like any other method.


Constructor Functions

When we declare a TypeScript, we’re declaring multiple things at the same time. We’re declaring an instance of the class, which is the entity with the code preceded with the class keyword. For example, we can write:

class Employee{  
  name: string;  
  age: number;  
  constructor(name: string, age: number) {  
    this.name = name;  
    this.age = age;  
  }
}  
let employee: Employee = new Employee('Jane', 20);

In the last line of the code snippet above, we’re using Employee as the type of the instances of the class Employee.

Also, we’re creating another value we call a constructor function. This is the function that’s called when the new keyword is being used to create a new instance of the class.

If we compile the TypeScript code above to ES5 or earlier, we can see we get something like the following code generated:

"use strict";  
var Employee = /** @class (function () {  
    function Employee(name, age) {  
        this.name = name;  
        this.age = age;  
    }  
    return Employee;  
}());  
var employee = new Employee('Jane', 20);

In the code above, the constructor function is the following code:

function Employee(name, age) {  
  this.name = name;  
  this.age = age;  
}

We have this because in JavaScript, no matter what version we’re using, a class is just the syntactic sugar for constructor functions. The inheritance model of TypeScript just extends from JavaScript. It didn’t change the inheritance model of JavaScript since it’s supposed to be 100% compatible with existing JavaScript code so existing JavaScript code can be adopted to use TypeScript.

In TypeScript, we can get the type of the constructor function with the typeof operator. It’s different from its usage in JavaScript as it has been extended to get the type of the constructor function of a class. If we run the following code …

class Employee{  
  name: string;  
  age: number;  
  static companyName: string = 'ABC Company';  
  constructor(name: string, age: number) {  
    this.name = name;  
    this.age = age;  
  }}

let employeeConstructor: typeof Employee = Employee;  
console.log(Employee.companyName);  
console.log(employeeConstructor.companyName);

… it makes sense when we get the value of the static member companyName logged in both console.log statements. As we can see from the compiled output …

"use strict";  
var Employee = /** @class */ (function () {  
    function Employee(name, age) {  
        this.name = name;  
        this.age = age;  
    }  
    Employee.companyName = 'ABC Company';  
    return Employee;  
}());  
var employeeConstructor = Employee;  
console.log(Employee.companyName);  
console.log(employeeConstructor.companyName);

… a JavaScript and TypeScript class are ultimately just functions. The class syntax makes inheritance of JavaScript easier to use since it looks like it’s using class-based inheritance, but it’s actually syntactic sugar on top of the prototypical inheritance model that’s been the same since JavaScript first came out.

TypeScript makes inheritance easy by letting us define abstract classes — where some implementation is done by the abstract and others are done in the subclass that extends the abstract class.

We also have abstract methods that subclasses can implement. Abstract methods only have the signature and return type and no implementation details.

Static members let us define members that are part of the class rather than an instance of the class.

It’s also important to know that the class syntax is ultimately syntactic sugar for the prototypical inheritance model that existed since the beginning of JavaScript. However, now we can put that aside since we can use the syntactic sugar to make it easier to understand and implement inheritance.

Categories
JavaScript TypeScript

TypeScript Advanced Types — Type Guards

TypeScript has many advanced type capabilities which make writing dynamically typed code easy. It also facilitates the adoption of existing JavaScript code since it lets us keep the dynamic capabilities of JavaScript while using the type-checking capabilities of TypeScript.

There are multiple kinds of advanced types in TypeScript, like intersection types, union types, type guards, nullable types, and type aliases, and more. In this article, we’ll look at type guards.


Type Guards

To check if an object is of a certain type, we can make our own type guards to check for members that we expect to be present and the data type of the values. To do this, we can use some TypeScript-specific operators and also JavaScript operators.

One way to check for types is to explicitly cast an object with a type with the as operator. This is needed for accessing a property that’s not specified in all the types that form a union type.

For example, if we have the following code:


interface Person {
  name: string;
  age: number;
}
interface Employee {
  employeeCode: string;
}
let person: Person | Employee = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
};
console.log(person.name);

Then the TypeScript compiler won’t let us access the name property of the person object since it’s only available in the Person type but not in the Employee type. Therefore, we’ll get the following error:

Property 'name' does not exist on type 'Person | Employee'.Property 'name' does not exist on type 'Employee'.(2339)

In this case, we have to use the type assertion operator available in TypeScript to cast the type to the Person object so that we can access the name property, which we know exists in the person object.

To do this, we use the as operator, as we do in the following code:

interface Person {
  name: string;
  age: number;
}
interface Employee {
  employeeCode: string;
}
let person: Person | Employee = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
};
console.log((person as Person).name);

With the as operator, we explicitly tell the TypeScript compiler that the person is of the Person class, so that we can access the name property which is in the Person interface.


Type Predicates

To check for the structure of the object, we can use a type predicate. A type predicate is a piece code where we check if the given property name has a value associated with it.

For example, we can write a new function isPerson to check if an object has the properties in the Person type:

interface Person {
  name: string;
  age: number;
}
interface Employee {
  employeeCode: string;
}
let person: Person | Employee = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
};
const isPerson = (person: Person | Employee): person is Person => {
  return (person as Person).name !== undefined;  
}
if (isPerson(person)) {
  console.log(person.name);  
}
else {
  console.log(person.employeeCode);  
}

In the code above, the isPerson returns a person is Person type, which is our type predicate.

If we use that function as we do in the code above, then the TypeScript compiler will automatically narrow down the type if a union type is composed of two types.

In the if (isPerson(person)){ ... } block, we can access any member of the Person interface.

However, this doesn’t work if there are more than two types that form the union type. For example, if we have the following code:

interface Animal {
  kind: string;
}
interface Person {
  name: string;
  age: number;
}
interface Employee {
  employeeCode: string;
}
let person: Person | Employee | Animal = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
};
const isPerson = (person: Person | Employee | Animal): person is Person => {
  return (person as Person).name !== undefined;  
}
if (isPerson(person)) {
  console.log(person.name);  
}
else {
  console.log(person.employeeCode);  
}

Then the TypeScript compiler will refuse to compile the code and we’ll get the following error messages:

Property 'employeeCode' does not exist on type 'Animal | Employee'.Property 'employeeCode' does not exist on type 'Animal'.(2339)

This is because it doesn’t know the type of what’s inside the else clause since it can be either Animal or Employee. To solve this, we can add another if block to check for the Employee type as we do in the following code:

interface Animal {
  kind: string;
}
interface Person {
  name: string;
  age: number;
}
interface Employee {
  employeeCode: string;
}
let person: Person | Employee | Animal = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
};
const isPerson = (person: Person | Employee | Animal): person is Person => {
  return (person as Person).name !== undefined;  
}
const isEmployee = (person: Person | Employee | Animal): person is Employee => {
  return (person as Employee).employeeCode !== undefined;  
}
if (isPerson(person)) {
  console.log(person.name);  
}
else if (isEmployee(person)) {
  console.log(person.employeeCode);  
}
else {
  console.log(person.kind);  
}

In Operator

Another way to check the structure to determine the data type is to use the in operator. It’s like the JavaScript in operator, where we can use it to check if a property exists in an object.

For example, to check if an object is a Person object, we can write the following code:

interface Animal {
  kind: string;
}
interface Person {
  name: string;
  age: number;
}
interface Employee {
  employeeCode: string;
}
let person: Person | Employee | Animal = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
};
const getIdentifier = (person: Person | Employee | Animal) => {
  if ('name' in person) {
    return person.name;
  }
  else if ('employeeCode' in person) {
    return person.employeeCode
  }
  return person.kind;
  
}

In the getIdentifier function, we used the in operator as we do in ordinary JavaScript code. If we check the name of a member that’s unique to a type, then the TypeScript compiler will infer the type of the person object in the if block as we have above.

Since name is a property that’s only in the Person interface, then the TypeScript compiler is smart enough to know that whatever inside is a Person object.

Likewise, since employeeCode is only a member of the Employee interface, then it knows that the person object inside is of type Employee.

If both types are eliminated, then the TypeScript compiler knows that it’s Animal since the other two types are eliminated by the if statements.


Typeof Type Guard

For determining the type of objects that have union types composed of primitive types, we can use the typeof operator.

For example, if we have a variable that has the union type number | string | boolean, then we can write the following code to determine whether it’s a number, a string, or a boolean. For example, if we write:

const isNumber = (x: any): x is number =>{
    return typeof x === "number";
}
const isString = (x: any): x is string => {
    return typeof x === "string";
}
const doSomething = (x: number | string | boolean) => {
  if (isNumber(x)) {
    console.log(x.toFixed(0));
  }
  else if (isString(x)) {
    console.log(x.length);
  }
  else {
    console.log(x);
  }
}
doSomething(1);

Then we can call number methods as we have inside the first if block since we used the isNumber function to help the TypeScript compiler determine if x is a number.

Likewise, this also goes for the string check with the isString function in the second if block.

If a variable is neither a number nor a string then it’s determined to be a boolean since we have a union of the number, string, and boolean types.

The typeof type guard can be written in the following ways:

  • typeof v === "typename"
  • typeof v !== "typename"

Where “typename” can be be "number", "string", "boolean", or "symbol".


Instanceof Type Guard

The instanceof type guard can be used to determine the type of instance type.

It’s useful for determining which child type an object belongs to, given the child type that the parent type derives from. For example, we can use the instanceof type guard like in the following code:

interface Animal {
  kind: string;
}
class Dog implements Animal{
  breed: string;
  kind: string;
  constructor(kind: string, breed: string) {    
    this.kind = kind;
    this.breed = breed;
  }
}
class Cat implements Animal{
  age: number;
  kind: string;
  constructor(kind: string, age: number) {    
    this.kind = kind;
    this.age = age;
  }
}
const getRandomAnimal = () =>{
  return Math.random() < 0.5 ?
    new Cat('cat', 2) :
    new Dog('dog', 'Laborador');
}
let animal = getRandomAnimal();
if (animal instanceof Cat) {
  console.log(animal.age);
}
if (animal instanceof Dog) {
  console.log(animal.breed);    
}

In the code above, we have a getRandomAnimal function that returns either a Cat or a Dog object, so the return type of it is Cat | Dog. Cat and Dog both implement the Animal interface.

The instanceof type guard determines the type of the object by its constructor, since the Cat and Dog constructors have different signatures, it can determine the type by comparing the constructor signatures.

If both classes have the same signature, the instanceof type guard will also help determine the right type. Inside the if (animal instanceof Cat) { ... } block, we can access the age member of the Cat instance.

Likewise, inside the if (animal instanceof Dog) {...} block, we can access the members that are exclusive to the Dog instance.


Conclusion

With various type guards and type predicates, the TypeScript compiler can narrow down the type with conditional statements.

Type predicate is denoted by the is keyword, like pet is Cat where pet is a variable and Cat is the type. We can also use the typeof type guard for checking primitive types, and the instanceof type guard for checking instance types.

Also, we have the in operator checking if a property exists in an object, which in turn determines the type of the object by the existence of the property.