Categories
JavaScript React TypeScript

How to Use the Optional Chaining Operator in Your React App Right Now

Optional chaining is a proposed feature that may be incorporated into the JavaScript specification.

The operator allows you to traverse through a nested object to get the value of variables without worrying if any of those will be undefined.

For example, without optional chaining, if you have the object below:

const person = {  
  name: 'Alice',  
  cat: {  
    name: 'Bob'  
  }  
};

If you want to get the cat’s name, you have to use the code below:

const catName = person.cat.name;

If cat is undefined or null in person, the JavaScript interpreter will throw an error. With the optional chaining operator, you can write:

const catName = person?.cat?.name;

If cat is undefined, the catName will be null.

It also works with keys of an object. Instead of const catName = person?.cat?.name;, we can write:

const catName = person?.['cat']?.['name'];

This syntax also works with functions. For example, you can write:

func?.('foo')

To call the function func with a string, where func may be undefined or null. If the function does not exist, it will not be run.

To further illustrate the example and to show you how to use it in a real application, we will build a React app that uses the NASA API, to get the latest asteroid data.

We will use the Create React App CLI program to build the app.

As optional chaining is just a proposed feature, it is not currently supported with the CLI, so we have to do some work ourselves by installing some packages and making some changes to the Babel configuration of the app to enable optional chaining.

To start, we run npx create-react-app nasa-app to create the project folder with the initial files.

Next, we install npm i -D @babel/plugin-proposal-optional-chaining customize-cra react-app-rewired to begin customizing Create React App to support the optional chaining syntax.

Next, we have to add new configuration files and edit existing ones to let our app run and build with the syntax.

First, we add a file called config-overrides.js and add the following:

const { useBabelRc, override, useEslintRc } = require("customize-cra");
module.exports = override(useBabelRc());

To let us use the .babelrc configuration file.

Then, we create the .babelrc file in the root folder of our project and add:

{  
    "plugins": [  
        [  
            "@babel/plugin-proposal-optional-chaining"  
        ],  
    ]  
}

This will add support for optional chaining syntax in our project.

Next, we have to switch to make our app run and build with react-app-rewired instead of the usual react-script program.

To do this, in the scripts section of package.json, we put:

"scripts": {  
    "start": "react-app-rewired start",  
    "build": "react-app-rewired build",  
    "test": "react-app-rewired test --env=jsdom",  
    "eject": "react-scripts eject"  
}

In here, we replaced the original scripts that use react-script to run and build our app, with react-app-rewired.

Now we can use the optional chaining syntax to build our app.

First, we have to install some packages.

Run npm i axios bootstrap react-bootstrap formik yup react-router-dom to install the axios HTTP client, React Bootstrap for styling, Formik and Yup for building forms and adding form validation, and React Router for routing URLs to the pages we build.

Now we can write some code. In App.js, we replace the existing code with:

import React from "react";  
import { Router, Route } from "react-router-dom";  
import HomePage from "./HomePage";  
import AsteroidsSearchPage from "./AsteroidsSearchPage";  
import { createBrowserHistory as createHistory } from "history";  
import "./App.css";  
import TopBar from "./TopBar";  
const history = createHistory();

function App() {  
  return (  
    <div className="App">  
      <Router history={history}>  
        <TopBar />  
        <Route path="/" exact component={HomePage} />  
        <Route path="/search" exact component={AsteroidsSearchPage} />  
      </Router>  
    </div>  
  );  
}export default App;

So that we get client-side routing to our pages. In App.css, we replace the existing code with:

.center {  
  text-align: center;  
}

Next, we start building new pages. Create a file called AsteroidSearchPage.js in the src folder and add:

import React, { useState, useEffect } from "react";  
import { Formik } from "formik";  
import Form from "react-bootstrap/Form";  
import Col from "react-bootstrap/Col";  
import Button from "react-bootstrap/Button";  
import * as yup from "yup";  
import Card from "react-bootstrap/Card";  
import "./AsteroidsSearchPage.css";  
import { searchFeed } from "./requests";

const schema = yup.object({  
  startDate: yup  
    .string()  
    .required("Start date is required")  
    .matches(  
      /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/,  
      "Invalid start date"  
    ),  
  endDate: yup  
    .string()  
    .required("End date is required")  
    .matches(  
      /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|\[12]\d|3\[01]))/,  
      "Invalid end date"  
    ),  
});

function AsteroidsSearchPage() {  
  const [feed, setFeed] = useState({});  
  const [error, setError] = useState("");
  const handleSubmit = async evt => {  
    const isValid = await schema.validate(evt);  
    if (!isValid) {  
      return;  
    }  
    try {  
      const response = await searchFeed(evt);  
      setFeed(response.data.near_earth_objects);  
    } catch (ex) {  
      alert(ex?.response?.data?.error_message);  
    }  
  };

  return (  
    <div className="AsteroidsSearchPage">  
      <h1 className="center">Search Asteroids</h1>  
      <Formik validationSchema={schema} onSubmit={handleSubmit}>  
        {({  
          handleSubmit,  
          handleChange,  
          handleBlur,  
          values,  
          touched,  
          isInvalid,  
          errors,  
        }) => (  
          <Form noValidate onSubmit={handleSubmit}>  
            <Form.Row>  
              <Form.Group as={Col} md="12" controlId="startDate">  
                <Form.Label>Start Date</Form.Label>  
                <Form.Control  
                  type="text"  
                  name="startDate"  
                  placeholder="YYYY-MM-DD"  
                  value={values.startDate || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.startDate && errors.startDate}  
                />  
                <Form.Control.Feedback type="invalid">  
                  {errors.startDate}  
                </Form.Control.Feedback>  
              </Form.Group>  
              <Form.Group as={Col} md="12" controlId="endDate">  
                <Form.Label>End Date</Form.Label>  
                <Form.Control  
                  type="text"  
                  name="endDate"  
                  placeholder="YYYY-MM-DD"  
                  value={values.endDate || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.startDate && errors.endDate}  
                /><Form.Control.Feedback type="invalid">  
                  {errors.endDate}  
                </Form.Control.Feedback>  
              </Form.Group>  
            </Form.Row>  
            <Button type="submit" style={{ marginRight: "10px" }}>  
              Search  
            </Button>  
          </Form>  
        )}  
      </Formik>  
      {Object.keys(feed)?.map(key => {  
        return (  
          <Card style={{ width: "90vw", margin: "0 auto" }}>  
            <Card.Body>  
              <Card.Title>{key}</Card.Title>  
              {feed?.[key]?.length > 0  
                ? feed?.[key]?.map(f => {  
                    return (  
                      <div style={{ paddingBottom: "10px" }}>  
                        {f?.close_approach_data?.length > 0 ? (  
                          <div>  
                            <b>  
                              Close Approach Date:  
                              {f?.close_approach_data?.map(d => {  
                                return <p>{d?.close_approach_date_full}</p>;  
                              })}  
                            </b>  
                          </div>  
                        ) : null}  
                        <div>  
                          Minimum Estimated Diameter:{" "}  
                          {  
                            f?.estimated_diameter?.kilometers  
                              ?.estimated_diameter_min  
                          }{" "}  
                          km  
                        </div>  
                        <div>  
                          Maximum Estimated Diameter:{" "}  
                          {  
                            f?.estimated_diameter?.kilometers  
                              ?.estimated_diameter_max  
                          }{" "}  
                          km  
                          <br />  
                        </div>  
                      </div>  
                    );  
                  })  
                : null}  
            </Card.Body>  
          </Card>  
        );  
      })}  
    </div>  
  );  
}

export default AsteroidsSearchPage;

This is where we add a form to search for asteroid data from the NASA API by date range. Both the start and end date fields should be in YYYY-MM-DD format and we changed our form validation to match that in the schema object.

Once validation is done, by calling the schema.validate function, we search. The response has many nested objects, so we use the optional chaining syntax everywhere in the result cards.

We loop through close_approach_data array, and we don’t assume it’s always defined or has a greater than zero length, the same goes for the call to the map function. We do not assume that the map function is always defined.

We also use the optional chaining syntax for f?.estimated_diameter?.kilometers ?.estimated_diameter_min and f?.estimated_diameter?.kilometers ?.estimated_diameter_max.

The more levels of nesting there are, the less likely it is that you can traverse the object tree successfully without the optional chaining syntax, as there is more chance of nested objects being undefined.

Also, note that the optional chaining syntax works for returned results like Object.keys(feed)

In AsteroidSearchPage.css, which we should create in the src folder, we put:

.AsteroidsSearchPage{  
  margin: 0 auto;  
  width: 90vw;  
}

To add some margins to the page.

Next, we build the home page. Create a file called HomePage.js in the src folder and add:

import React, { useState, useEffect } from "react";  
import { browse } from "./requests";  
import Card from "react-bootstrap/Card";  
import "./HomePage.css";

function HomePage() {  
  const [initialized, setIntialized] = useState(false);  
  const [feed, setFeed] = useState([]);
  const browserFeed = async () => {  
    const response = await browse();  
    setFeed(response.data.near_earth_objects);  
    setIntialized(true);  
  };

  useEffect(() => {  
    if (!initialized) {  
      browserFeed();  
    }  
  });  
  return (  
    <div className="HomePage">  
      <h1 className='center'>Asteroids Close to Earth</h1>  
      <br />  
      {feed?.map(f => {  
        return (  
          <Card style={{ width: "90vw", margin: "0 auto" }}>  
            <Card.Body>  
              <Card.Title>{f?.name}</Card.Title>  
              <div>  
                {f?.close_approach_data?.length > 0 ? (  
                  <div>  
                    Close Approach Date:  
                    {f?.close_approach_data?.map(d => {  
                      return <p>{d?.close_approach_date_full}</p>;  
                    })}  
                  </div>  
                ) : null}  
                <p>  
                  Minimum Estimated Diameter:{" "}  
                  {f?.estimated_diameter?.kilometers?.estimated_diameter_min} km  
                </p>  
                <p>  
                  Maximum Estimated Diameter:{" "}  
                  {f?.estimated_diameter?.kilometers?.estimated_diameter_max} km  
                </p>  
              </div>  
            </Card.Body>  
          </Card>  
        );  
      })}  
    </div>  
  );  
}

export default HomePage;

This page is very similar to the AsteroidSearchPage component with the use of the optional chaining syntax.

Next, create HomePage.css in the src folder and add:

.HomePage{  
  text-align: left;  
}

To align our text to the left.

Next, we create requests.js in the src folder and add:

const APIURL = "https://api.nasa.gov/neo/rest/v1]";  
const axios = require("axios");

export const searchFeed = data =>  
  axios.get(  
    `${APIURL}/feed?start_date=${data.startDate}&end_date=${data.endDate}&api_key=${process.env.REACT_APP_APIKEY}`  
  );

export const browse = () =>  
  axios.get(`${APIURL}/neo/browse?api_key=${process.env.REACT_APP_APIKEY}`);

To add the functions for making the HTTP requests to the NASA API for getting asteroid data and searching for them.

process.env.REACT_APP_APIKEY has the API key when you add the API key to the .env file of your project with REACT_APP_APIKEY as the key. Register for an API key at NASA.

Finally, we add TopBar.js to the src folder and add:

import React from "react";  
import Navbar from "react-bootstrap/Navbar";  
import Nav from "react-bootstrap/Nav";  
import { withRouter } from "react-router-dom";

function TopBar({ location }) {  
  const { pathname } = location;  
  return (  
    <Navbar bg="primary" expand="lg" variant="dark">  
      <Navbar.Brand href="#home">NASA App</Navbar.Brand>  
      <Navbar.Toggle aria-controls="basic-navbar-nav" />  
      <Navbar.Collapse id="basic-navbar-nav">  
        <Nav className="mr-auto">  
          <Nav.Link href="/" active={pathname == "/"}>  
            Home  
          </Nav.Link>  
          <Nav.Link href="/search" active={pathname.includes("/search")}>  
            Search  
          </Nav.Link>  
        </Nav>  
      </Navbar.Collapse>  
    </Navbar>  
  );  
}

export default withRouter(TopBar);

This is the navigation bar at the top of each page in our app. We set the active prop by checking the current URL of the page so we get highlights in our links.

Categories
JavaScript TypeScript

TypeScript Data Types – Null, Void, Undefined, Never and Object

JavaScript, like any other programming language, has its own data structures and types.

JavaScript has a few data types that we have to know, 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. In this article, we’ll look at the void, null, undefined, never, and the object types.


Void

The void type is pretty much the opposite of the any type. It means the absence of any type. So, the variable of the void type can only have the value null if the --strictNullChecks setting isn’t specified when running the TypeScrip compiler or it can be set to undefined.

Therefore, directly assigning values to a void variable isn’t very useful. It’s more used for specifying the return data type of a function. A function that has the void return type doesn’t return anything.

For example, we can do a useless assignment to a void variable like in the following code:

let useless: void = undefined;

We can set the return type of an arrow function to void by writing:

const voidFn = (): void => {  
  console.log("Void function returns nothing");  
}
voidFn();

Alternatively, we can set the return type to void for a traditional function like in the following code:

function voidFn(): void {  
  console.log("Void function returns nothing");  
}
voidFn();

Both function calls will output Void function returns nothing from the console.log statement inside the function.


Null

The null type represents a variable that can only take on the value null. null means that there’s no value for a variable.

So, once again, assigning a value to it is pretty useless. We can only assign null to a variable that has the variable null. With the --strictNullChecks flag set when running the TypeScript compiler, null can only be assignable to variables with the any type and the null type.

Where it does become useful is that we can have variables that can have values from more than one assigned to it with union types.

Then, we can specify something useful along with the null type. This is different from JavaScript in that the value null is the type object instead of null for historical reasons.


Undefined

The undefined type represents a variable that can only take on the value undefined. So, once again, assigning a value to it is pretty useless.

We can only assign undefined to a variable that has the variable null. undefined can only be assignable to variables with the any type and the undefined type.

Where it does become useful is that we can have variables that can have values from more than one assigned to it with union types.

Then, we can specify something useful along with the undefined type. It’s practically useless on its own, so we shouldn’t see many cases with variables that have only the undefined type.


Never

The never type is a type of value that represents something that never occurs. It’s like void in that it’s useful for designating that a function never returns anything.

The never type is a sub-type of, and is assignable to, every type. However, no type is a sub-type of, or assignable to, the never type except for other never variables.

A function that has a never return type must always have an unreachable endpoint. For example, we can write a function that has an infinite loop that has the never return type like in the following code:

function infiniteFn(): never {  
  while (true) {  
  }  
}

A function that throws an exception may also have the never return type, like in the following example:

function errorFn(message: string): never {  
  throw new Error(message);  
}
errorFn('Error occurred');

Object

The object type is a type that represents non-primitive objects. That is, anything that’s not a number, string, boolean, bigint, symbol, null, or undefined.

It’s mainly used in the type definition of the Object object in the standard library and other pieces of code that don’t want primitive values to be assigned to it, or passed into a function.

For example, in the type definition of the Object.create method, we see that the type of the parameter is set to the object like in the following code:

create(o: object | null): any;

Likewise, in the signature of the setPrototypeOf method in the same type declaration, we see that the proto parameter, which is the parameter that takes the prototype of an object, also has the object type set, as it does below:

setPrototypeOf(o: any, proto: object | null): any;

This way, we can’t pass in primitive values into these methods as arguments in TypeScript. If we did, then we would get an error. So, if we have the following code in our TypeScript files, then they would be compiled and run:

const obj1 = Object.create({});      
const obj2 = Object.create(null);  
console.log(obj1);  
console.log(obj2);

We would get an empty object in both console.log statements.

The only difference is that obj2 is a pure object, which means it doesn’t inherit from any prototype. Passing in primitive values to the create method like in the code below will cause compilation to fail:

Object.create(42);   
Object.create("abc");   
Object.create(false);  
Object.create(undefined)

The code above would get us an Argument of type ‘42’ is not assignable to parameter of type ‘object | null’ error for the first one, Argument of type ‘abc’ is not assignable to parameter of type ‘object | null’ for the second one.

The third line would get us Argument of type ‘false’ is not assignable to parameter of type ‘object | null’, and the last line would get us Argument of type ‘undefined’ is not assignable to parameter of type ‘object | null’.

Compilation would fail and the code wouldn’t run.

Also, we can use it to prevent primitive values from being assigned to it. For example, if we write:

let x: object = {};

Then, the code above would be compiled and run. However, if we write the following instead:

let x: object = 1;

Then we get Type ‘1’ is not assignable to type ‘object’ and the code can’t be compiled with the TypeScript compiler and be run, since 1 is a primitive value.

The void type is pretty much the opposite of the any type. It means the absence of any type. So, the variable of the void type can only have the value null if the --strictNullChecks setting isn’t specified when running the TypeScrip compiler or it can be set to undefined.

It’s useful for declaring functions that don’t return anything and not much else. The null type variable can only take on the null value. The undefined type can only be assigned the value undefined.

The object type is a type that represents non-primitive objects. That is, anything that’s not a number, string, boolean, bigint, symbol, null, or undefined.

It’s mainly used in the type definition of the Object object in the standard library and other pieces of code that don’t want primitive values to be assigned to it, or passed into a function.

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

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.