Categories
Quasar

Developing Vue Apps with the Quasar Library — Row Selection and Column Visibility

Quasar is a popular Vue UI library for developing good looking Vue apps.

In this article, we’ll take a look at how to create Vue apps with the Quasar UI library.

Row Selection

We can enable row select with the selection and selected.sync props.

To do this, we write:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-layout
        view="lHh Lpr lFf"
        container
        style="height: 100vh;"
        class="shadow-2 rounded-borders"
      >
        <div class="q-pa-md">
          <q-table
            title="Treats"
            :data="data"
            :columns="columns"
            row-key="name"
            selection="single"
            :selected.sync="selected"
          >
          </q-table>
          <div class="q-mt-md">
            {{ selected }}
          </div>
        </div>
      </q-layout>
    </div>
    <script>
      const columns = [
        {
          name: "name",
          required: true,
          label: "Dessert",
          align: "left",
          field: (row) => row.name,
          format: (val) => `${val}`,
          sortable: true
        },
        {
          name: "calories",
          align: "center",
          label: "Calories",
          field: "calories",
          sortable: true
        },
        { name: "fat", label: "Fat (g)", field: "fat", sortable: true },
        {
          name: "calcium",
          label: "Calcium (%)",
          field: "calcium",
          sortable: true,
          sort: (a, b) => parseInt(a, 10) - parseInt(b, 10)
        }
      ];

      const data = [
        {
          name: "Frozen Yogurt",
          calories: 159,
          fat: 6.0,
          calcium: "14%"
        },
        {
          name: "Ice cream sandwich",
          calories: 237,
          fat: 9.0,
          calcium: "8%"
        },
        {
          name: "Eclair",
          calories: 262,
          fat: 16.0,
          calcium: "6%"
        },
        {
          name: "Honeycomb",
          calories: 408,
          fat: 3.2,
          calcium: "0%"
        },
        {
          name: "Donut",
          calories: 452,
          fat: 25.0,
          calcium: "2%"
        },
        {
          name: "KitKat",
          calories: 518,
          fat: 26.0,
          calcium: "12%"
        }
      ];

      new Vue({
        el: "#q-app",
        data: {
          columns,
          data,
          selected: []
        }
      });
    </script>
  </body>
</html>

selection is set to 'single' so that only one row can be selected at a time.

selected has the selected row’s data in an array.

We can set selection to 'multiple' to enable multiple selection.

Visible Columns

We can set the columns that are visible in a Quasar table with the visible-columns prop:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-layout
        view="lHh Lpr lFf"
        container
        style="height: 100vh;"
        class="shadow-2 rounded-borders"
      >
        <div class="q-pa-md">
          <q-table
            title="Treats"
            :data="data"
            :columns="columns"
            row-key="name"
            :visible-columns="['dessert','calories']"
          >
          </q-table>
        </div>
      </q-layout>
    </div>
    <script>
      const columns = [
        {
          name: "name",
          required: true,
          label: "Dessert",
          align: "left",
          field: (row) => row.name,
          format: (val) => `${val}`,
          sortable: true
        },
        {
          name: "calories",
          align: "center",
          label: "Calories",
          field: "calories",
          sortable: true
        },
        { name: "fat", label: "Fat (g)", field: "fat", sortable: true },
        {
          name: "calcium",
          label: "Calcium (%)",
          field: "calcium",
          sortable: true,
          sort: (a, b) => parseInt(a, 10) - parseInt(b, 10)
        }
      ];

      const data = [
        {
          name: "Frozen Yogurt",
          calories: 159,
          fat: 6.0,
          calcium: "14%"
        },
        {
          name: "Ice cream sandwich",
          calories: 237,
          fat: 9.0,
          calcium: "8%"
        },
        {
          name: "Eclair",
          calories: 262,
          fat: 16.0,
          calcium: "6%"
        },
        {
          name: "Honeycomb",
          calories: 408,
          fat: 3.2,
          calcium: "0%"
        },
        {
          name: "Donut",
          calories: 452,
          fat: 25.0,
          calcium: "2%"
        },
        {
          name: "KitKat",
          calories: 518,
          fat: 26.0,
          calcium: "12%"
        }
      ];

      new Vue({
        el: "#q-app",
        data: {
          columns,
          data,
        }
      });
    </script>
  </body>
</html>

The visible-columns prop has an array with the property name of the columns we want to show.

The columns that have the required property set to true in the column definition can’t be hidden.

Conclusion

We can enable row selection and change the visibility of columns with Quasar’s q-table component.

Categories
Quasar

Developing Vue Apps with the Quasar Library — Virtual Scrolling and Custom Row

Quasar is a popular Vue UI library for developing good looking Vue apps.

In this article, we’ll take a look at how to create Vue apps with the Quasar UI library.

Virtual Scrolling

With q-table ‘s virtual-scroll prop, we can add virtual scrolling to our table.

It improves the performance of the table when we need to display lots of data by loading only the ones that are displayed.

For instance, we can write:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-layout
        view="lHh Lpr lFf"
        container
        style="height: 100vh;"
        class="shadow-2 rounded-borders"
      >
        <div class="q-pa-md">
          <q-table
            style="height: 400px;"
            title="Treats"
            :data="data"
            :columns="columns"
            row-key="index"
            virtual-scroll
            :pagination.sync="pagination"
            :rows-per-page-options="[0]"
          >
          </q-table>
        </div>
      </q-layout>
    </div>
    <script>
      const columns = [
        {
          name: "name",
          required: true,
          label: "Dessert",
          align: "left",
          field: (row) => row.name,
          format: (val) => `${val}`,
          sortable: true
        },
        {
          name: "calories",
          align: "center",
          label: "Calories",
          field: "calories",
          sortable: true
        },
        { name: "fat", label: "Fat (g)", field: "fat", sortable: true },
        {
          name: "calcium",
          label: "Calcium (%)",
          field: "calcium",
          sortable: true,
          sort: (a, b) => parseInt(a, 10) - parseInt(b, 10)
        }
      ];

      const seed = [
        {
          name: "Frozen Yogurt",
          calories: 159,
          fat: 6.0,
          calcium: "14%"
        },
        {
          name: "Ice cream sandwich",
          calories: 237,
          fat: 9.0,
          calcium: "8%"
        },
        {
          name: "Eclair",
          calories: 262,
          fat: 16.0,
          calcium: "6%"
        },
        {
          name: "Honeycomb",
          calories: 408,
          fat: 3.2,
          calcium: "0%"
        },
        {
          name: "Donut",
          calories: 452,
          fat: 25.0,
          calcium: "2%"
        },
        {
          name: "KitKat",
          calories: 518,
          fat: 26.0,
          calcium: "12%"
        }
      ];

      let data = [];
      for (let i = 0; i < 1000; i++) {
        data.push(...[...seed]);
      }
      data.forEach((row, index) => {
        row.index = index;
      });

      new Vue({
        el: "#q-app",
        data: {
          columns,
          data,
          pagination: {
            rowsPerPage: 0
          }
        }
      });
    </script>
  </body>
</html>

To enable pagination, we set rowsPerPage to a number bigger than 0:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-layout
        view="lHh Lpr lFf"
        container
        style="height: 100vh;"
        class="shadow-2 rounded-borders"
      >
        <div class="q-pa-md">
          <q-table
            style="height: 400px;"
            title="Treats"
            :data="data"
            :columns="columns"
            row-key="index"
            virtual-scroll
            :pagination.sync="pagination"
            :rows-per-page-options="[0]"
          >
          </q-table>
        </div>
      </q-layout>
    </div>
    <script>
      const columns = [
        {
          name: "name",
          required: true,
          label: "Dessert",
          align: "left",
          field: (row) => row.name,
          format: (val) => `${val}`,
          sortable: true
        },
        {
          name: "calories",
          align: "center",
          label: "Calories",
          field: "calories",
          sortable: true
        },
        { name: "fat", label: "Fat (g)", field: "fat", sortable: true },
        {
          name: "calcium",
          label: "Calcium (%)",
          field: "calcium",
          sortable: true,
          sort: (a, b) => parseInt(a, 10) - parseInt(b, 10)
        }
      ];

      const seed = [
        {
          name: "Frozen Yogurt",
          calories: 159,
          fat: 6.0,
          calcium: "14%"
        },
        {
          name: "Ice cream sandwich",
          calories: 237,
          fat: 9.0,
          calcium: "8%"
        },
        {
          name: "Eclair",
          calories: 262,
          fat: 16.0,
          calcium: "6%"
        },
        {
          name: "Honeycomb",
          calories: 408,
          fat: 3.2,
          calcium: "0%"
        },
        {
          name: "Donut",
          calories: 452,
          fat: 25.0,
          calcium: "2%"
        },
        {
          name: "KitKat",
          calories: 518,
          fat: 26.0,
          calcium: "12%"
        }
      ];

      let data = [];
      for (let i = 0; i < 1000; i++) {
        data.push(...[...seed]);
      }
      data.forEach((row, index) => {
        row.index = index;
      });

      new Vue({
        el: "#q-app",
        data: {
          columns,
          data,
          pagination: {
            rowsPerPage: 1000
          }
        }
      });
    </script>
  </body>
</html>

Multiple Rows for a Data Row

We can customize the rows by populating the header and body slots.

For instance, we can write:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-layout
        view="lHh Lpr lFf"
        container
        style="height: 100vh;"
        class="shadow-2 rounded-borders"
      >
        <div class="q-pa-md">
          <q-table
            style="height: 400px;"
            title="Treats"
            :data="data"
            :columns="columns"
            row-key="index"
            virtual-scroll
            :pagination.sync="pagination"
            :rows-per-page-options="[0]"
          >
            <template v-slot:header="props">
              <q-tr :props="props">
                <q-th></q-th>
                <q-th v-for="col in props.cols" :key="col.name" :props="props">
                  {{ col.label }}
                </q-th>
              </q-tr>
            </template>

            <template v-slot:body="props">
              <q-tr :props="props" :key="`m_${props.row.index}`">
                <q-td>
                  Name: {{ props.row.name }}
                </q-td>
                <q-td v-for="col in props.cols" :key="col.name" :props="props">
                  {{ col.value }}
                </q-td>
              </q-tr>
              <q-tr
                :props="props"
                :key="`e_${props.row.index}`"
                class="q-virtual-scroll--with-prev"
              >
                <q-td colspan="100%">
                  <div class="text-left">
                    This is the second row generated from the same data: {{
                    props.row.name }}
                  </div>
                </q-td>
              </q-tr>
            </template>
          </q-table>
        </div>
      </q-layout>
    </div>
    <script>
      const columns = [
        {
          name: "name",
          required: true,
          label: "Dessert",
          align: "left",
          field: (row) => row.name,
          format: (val) => `${val}`,
          sortable: true
        },
        {
          name: "calories",
          align: "center",
          label: "Calories",
          field: "calories",
          sortable: true
        },
        { name: "fat", label: "Fat (g)", field: "fat", sortable: true },
        {
          name: "calcium",
          label: "Calcium (%)",
          field: "calcium",
          sortable: true,
          sort: (a, b) => parseInt(a, 10) - parseInt(b, 10)
        }
      ];

      const data = [
        {
          name: "Frozen Yogurt",
          calories: 159,
          fat: 6.0,
          calcium: "14%"
        },
        {
          name: "Ice cream sandwich",
          calories: 237,
          fat: 9.0,
          calcium: "8%"
        },
        {
          name: "Eclair",
          calories: 262,
          fat: 16.0,
          calcium: "6%"
        },
        {
          name: "Honeycomb",
          calories: 408,
          fat: 3.2,
          calcium: "0%"
        },
        {
          name: "Donut",
          calories: 452,
          fat: 25.0,
          calcium: "2%"
        },
        {
          name: "KitKat",
          calories: 518,
          fat: 26.0,
          calcium: "12%"
        }
      ];

      new Vue({
        el: "#q-app",
        data: {
          columns,
          data,
          pagination: {
            rowsPerPage: 1000
          }
        }
      });
    </script>
  </body>
</html>

We populate the body slot with the table cell data.

We add another tr to display whatever data in the 2nd row.

And the header slot has the table header cells.

Conclusion

We can add virtual scrolling and customize the row display with Quasat’s q-table component.

Categories
TypeScript

How does TypeScript Know Which Types are Compatible with Each Other? — Enums and More

The advantage of TypeScript over JavaScript is that we can annotate data types of our variables, functions, and other entities with it. This lets us get auto-completion in our text editor and it also adds a compile step to building apps which means that compile-time checks can catch more errors like undefined error and unexpected data type errors that would otherwise be caught at runtime.

To do this, TypeScript checks if different variables have the same type according to the structure and other aspects of an entity, like the number of parameters and the type of each parameter and so on. In this article, we’ll continue to look at how TypeScript determines which data types are compatible with each other.

Determining Compatible Function Parameter Types

When passing in values into functions as arguments, the parameter assignment will succeed if either the source parameter is assignable to the target parameter or vice versa. This involves having actions that can’t be determined at compile-time but it’s still allowed. This means that we can pass in arguments that are of a more specialized type than the one specified by a parameter to maintain compatibility with JavaScript. For example, we can do something like in the following code:

interface Person {
  name: string;
  age: number;
}

interface Employee {
  name: string;
  age: number;
  employeeCode: string;
}

const fn = (person: Person) => person;
const employee: Employee = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
}

fn(employee);

In the code above, we passed in a variable of the type Employee , which has more properties than the person parameter, which has the type Person . This means that, like in JavaScript, we can choose to ignore properties of objects that are passed into functions. Of course, the employeeCode property can’t be accessed inside the fn function, so if we write:

interface Person {
  name: string;
  age: number;
}

interface Employee {
  name: string;
  age: number;
  employeeCode: string;
}

const fn = (person: Person) => console.log(person.employeeCode);
const employee: Employee = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
}

fn(employee);123'
}

fn(employee);

We would get the error message:

Property 'employeeCode' does not exist on type 'Person'.(2339)

To disallow this behavior, we can enable the strictFunctionTypes flag when we compile the code.

Optional Parameters and Rest Parameters

In TypeScript, optional and required parameters are interchangeable for the sake of function parameter type compatibility checks. Extra parameters of the source type isn’t an error, optional parameters of the target type without corresponding parameters of the source type is also not an error. For example, the following code is allowed:

const runLater = (callbackFn: (...args: any[]) => void) => 0;
runLater((a, b) => { });
runLater((a, b, c) => { });

We can pass in any function as a callback function since we used the rest operation to specify that we can pass in any number of arguments in the callback function that we pass to the runLater function. Functions with rest parameters are treated as if it has an infinite number of parameters. This is OK since not passing in anything to a function if the same as passing in undefined in JavaScript.

Overloaded Functions

Functions overloads, where there’re multiple signatures for a function with the same name, must have arguments that match at least one of the signatures for TypeScript to accept the function call as valid. For example, if we have the following function overload:

function fn(a: { b: number, c: string, d: boolean }): number
function fn(a: { b: number, c: string }): number
function fn(a: any): number {
  return a.b;
};

Then we have to call the fn function by passing in at a number for the first argument and a string for the second argument like we do below:

fn({ b: 1, c: 'a' });

TypeScript will check against each signature to see if the property structure of the object we pass in as the argument matches any of the overloads. If it doesn’t match like if we have the following:

fn({ b: 1 });

Then we get the following error message:

No overload matches this call.

Overload 1 of 2, '(a: { b: number; c: string; d: boolean; }): number', gave the following error.

Argument of type '{ b: number; }' is not assignable to parameter of type '{ b: number; c: string; d: boolean; }'.

Type '{ b: number; }' is missing the following properties from type '{ b: number; c: string; d: boolean; }': c, d

Overload 2 of 2, '(a: { b: number; c: string; }): number', gave the following error.

Argument of type '{ b: number; }' is not assignable to parameter of type '{ b: number; c: string; }'.

Property 'c' is missing in type '{ b: number; }' but required in type '{ b: number; c: string; }'.(2769)
input.ts(2, 29): 'c' is declared here.

Enums

In TypeScript, enums are considered to be compatible with numbers and vice versa. However, enum values from different enum types are considered incompatible. For example, if we have:

enum Fruit { Orange, Apple };
enum Bird { Chicken, Duck, Goose };

let fruit = Fruit.Orange;
fruit = Bird.Chicken;

Then in the last line, we’ll get the error message:

Type 'Bird.Chicken' is not assignable to type 'Fruit'.(2322)

Classes

Type compatibility of classes works like object literal types. However, only instance members of each class are checked for compatibility. Static members are ignored for type compatibility checks. For example, if we have:

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  static getNumPersons() {
    return 1;
  }
}

let animal: Animal = new Animal('Jane');
let person: Person = new Person('Joe');
animal = person;

Then the code would be accepted by the TypeScript compiler since animal and person are considered to be variables with compatible types. Static members like constructor and the getNumPersons method is ignored when the TypeScript compiler checks for type compatibility.

Private and protected members are checked during type compatibility checks when they’re included in a class. For example, if we have the following code:

class Animal {
  name: string;
  private age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  static getNumPersons() {
    return 1;
  }
}

let animal: Animal = new Animal('Jane');
let person: Person = new Person('Joe');
animal = person;

Then the TypeScript will reject the code with the following error:

Property 'age' is missing in type 'Person' but required in type 'Animal'.(2741)

input.ts(3, 11): 'age' is declared here.

TypeScript will only accept the code if the private member of both classes are derived from the same source. For example, if we have:

class Animal {
  private age: number = 20;
}

class Dog extends Animal {
  name: string;
  constructor(name: string) {
    super();
    this.name = name;
  }
}

class Cat extends Animal{
  name: string;
  constructor(name: string) {
    super();
    this.name = name;
  }
}

let dog: Dog = new Dog('Jane');
let cat: Cat = new Cat('Joe');
dog = cat;

Then the TypeScript compiler will accept the code when we try to compile it since the private member age is in the Animal class which the Dog and Cat classes are sub-classes of. This also to apply to protected members.

Generics

Type compatibility checks for generics are also derived from their structure. For example, if we have:

interface Person<T> {
  name: string;
}
let x: Person<number> = { name: 'Joe' };
let y: Person<string> = { name: 'Jane' };
x = y;

Then they’re compatible since both x and y are of type Person and we didn’t reference the generic type marker in any member of the interface. However, if we do reference a member in the Person interface, as we do in the following code:

interface Person<T> {
  name: string;
  age: T
}
let x: Person<number> = { name: 'Joe', age: 10 };
let y: Person<string> = { name: 'Jane', age: '20' };
x = y;

Then the type we pass into the <> does matter since we’re using it to determine the type of age . If we try to compile the code above, we would get the error:

Type 'Person<string>' is not assignable to type 'Person<number>'.

Type 'string' is not assignable to type 'number'.(2322)

For them to be considered to be compatible types, the type we pass in have to be the same in both variable declarations as we have below:

interface Person<T> {
  name: string;
  age: T
}

let x: Person<number> = { name: 'Joe', age: 10 };
let y: Person<number> = { name: 'Jane', age: 20 };
x = y;

In TypeScript code, we can pass in arguments that are of a more specialized type than the one specified by a parameter to maintain compatibility with JavaScript. For functions with overloads, then the arguments that we pass in have to match the parameter types that are declared in the function overloads. For classes, only instance members are considered to when determining if 2 classes are of compatible types. This applies to public members. For private and protected members, they have to be derived from the same super-class for 2 classes to be determined to have compatible types. Enums are compatible with numbers and vice versa, but enum values from 2 different enums aren’t compatible with each other. Generics are considered compatible with each by following the same rules as objects, functions, and classes. The types that are passed into a generic will be considered like any other code for type compatibility.

Categories
TypeScript

How does TypeScript Know Which Types are Compatible with Each Other? — Objects and Functions

The advantage of TypeScript over JavaScript is that we can annotate data types of our variables, functions, and other entities with it. This lets us get auto-completion in our text editor and it also adds a compile step to building apps which means that compile-time checks can catch more errors like undefined error and unexpected data type errors that would otherwise be caught at runtime.

To do this, TypeScript checks if different variables have the same type according to the structure and other aspects of an entity, like the number of parameters and the type of each parameter and so on. In this article, we’ll look at how TypeScript determines which data types are compatible with each other.

TypeScript’s type system allows some operations that can’t be known at compile-time to be safe. A type system is sound when it doesn’t allow actions that aren’t known at compile time. However, since TypeScript is a superset of JavaScript, it has to allow some unsound actions to be done at compile-time.

Basic Type Compatibility

The most basic rule that TypeScript used to determine type compatibility is that if both types have the same structure, then it’s considered to be compatible types. Having the same structure means that both types have the same member names and each member has the same type. For example, if we have:

interface A {
  name: string;
  age: number;
}

interface B {
  name: string;
  age: number;
}

let x: A = { name: 'Joe', age: 10 };
let y: B = { name: 'Jane', age: 20 };
x = y;

Then TypeScript would accept the code and compile it since both interfaces A and B both have name and age as their fields and both name and age in both interfaces have the same type. Also, if an object has all the properties of a type and extra properties that aren’t in the interface, then the object can also be assigned to a variable without a type annotation, which can then be assigned to a variable with a set type. For instance, if we have the following code:

interface A {
  name: string;
  age: number;
}

let y: A;
let x = { name: 'Joe', age: 10, gender: 'male' };
y = x;

Where y has the data type A , then we can assign an object to a variable with the without a data type annotation, which is x , and then we can assign to y which has the type A . This is because only object literals have the excess property check. Variables do not have this check.

Likewise, data types of functions are also checked when assigning a function to a variable. We can pass in an object that has more properties than the type that type of the parameter. For example, we can write something like the following code:

interface Person {
  name: string;
  age: number;
}

let person = { name: 'Joe', age: 10, gender: 'male' };

function echo(person: Person) {
  return person;
}

echo(person);

The person object has a gender property that isn’t in the Person interface. but the person parameter in the echo function is of type Person . Given that, we can still pass in person into as the first argument of the echo function call. TypeScript doesn’t care if there’re extra properties in the argument that we pass in. On the other hand, if our argument has missing properties, then TypeScript will give us an error and won’t compile the code. For instance, if we have the following code:

interface Person {
  name: string;
  age: number;
}

let person = { name: 'Joe' };

function echo(person: Person) {
  return person;
}

echo(person);

Then we would get the error:

Argument of type '{ name: string; }' is not assignable to parameter of type 'Person'.

Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.(2345)
input.ts(3, 3): 'age' is declared here.

The structural check for type compatibility is done recursively. So the same checks apply to objects with whatever nesting level it has. For example, if we have:

interface Person {
  name: string;
  age: number;
  address: {
    street: string;
  }
}

let person = { name: 'Joe', age: 20, address: {street: '123 A St.'} };

function echo(person: Person) {
  return person;
}

echo(person);

This would pass the structural type checking done by TypeScript because all the properties at each level are present. On the other hand, if we have:

interface Person {
  name: string;
  age: number;
  address: {
    street: string;
  }
}

let person = { name: 'Joe', age: 20, address: { } };

function echo(person: Person) {
  return person;
}

echo(person);

Then the TypeScript compiler would give us the following error:

Argument of type '{ name: string; age: number; address: {}; }' is not assignable to parameter of type 'Person'.

Types of property 'address' are incompatible.

Property 'street' is missing in type '{}' but required in type '{ street: string; }'.(2345)

input.ts(5, 5): 'street' is declared here.

since the street property is missing from the object assigned to the address property.

Comparing Functions

Comparing objects and primitive values for their types are pretty straightforward. However, determining which functions have types that are compatible with each other is harder. In TypeScript, we can assign a function that has a function that has fewer parameters to one with a function that has more parameters but otherwise have the same function signature. For example, if we have:

let x = (a: number) => 0;
let y = (a: number, b: string) => 0;

Then we can assign x to y like we do in the following code:

y = x;

This is because, in JavaScript, we can often assign functions that should have more parameters by default by ones with fewer parameters. For example, the array map method takes a callback function which has 2 parameters which are the array entry that’s being processed and the array index of the entry. However, we sometimes just pass in a callback function that only has the first parameter like in the following code:

let arr = ['a', 'b', 'c'];
arr.map(a => a);

TypeScript has to allow for the discarding of parameters to maintain compatibility with JavaScript. Likewise, for comparing return types, TypeScript determines that a function with a return type that has more properties is compatible with ones with fewer properties but otherwise has the same structure. For example, if we have 2 functions like in the following code:

let fn1 = () => ({ name: "Alice" });
let fn2 = () => ({ name: "Joe", age: 20 });

Then we can assign fn2 to fn1 like in the following code:

fn1 = fn2;

However, the other way around wouldn’t work:

fn2 = fn1;

If we try to compile the code above, we would get the following error from the TypeScript compiler:

Type '() => { name: string; }' is not assignable to type '() => { name: string; age: number; }'.

Property 'age' is missing in type '{ name: string; }' but required in type '{ name: string; age: number; }'.(2322)

input.ts(2, 33): 'age' is declared here.

As we can see, TypeScript accepts a return type that has more properties as ones that have fewer properties but otherwise have the same structure.

TypeScript checks the data type of objects and functions by their structure. Generally, if 2 types have the same properties and data types for each, then they’re considered to be the same types. Otherwise, objects that have more properties than another one, but otherwise have the same structure are also considered to be compatible. Likewise, if a function has more parameters than another one with fewer parameters, but otherwise have the same structure, than they’re considered the same. This also applies to the structure of objects that are returned by functions. If one function returns an object has more properties than another function which returns an object with fewer properties than the other function, but otherwise have the same structure, then the 2 return types are considered to be compatible.

Categories
JavaScript

What Are JavaScript Constructor Functions?

Learn about functions that return functions

In JavaScript, functions can be used as templates for creating other objects. These functions are called constructor functions and have special properties that are different from regular functions.

Defining Constructor Functions

They can be called with the new keyword to construct a new instance of an object from constructor functions.

Also, they’re different from regular functions in that their name should start with a capital letter.

A simple example of a constructor function would be the following:

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

It’s important to note that we used the function keyword to define the constructor function. We need to use it because arrow functions can’t be used to define constructor functions.

this is an object that has the data of the object instance created by the new keyword.

With this function, we can create a new Person object by writing:

let person = new Person('Joe', 10);

In the Person function, this.name and this.age are properties of object instances that are created with the new operator.

this.name of the person object would have the value 'Joe', and this.age would have the value 10.

If we create another instance of the Person object by writing …

let jane = new Person('Jane', 11);

… then the this.name of the jane object would have the value 'Jane', and this.age would have the value 11.

Every instance of the Person constructor will have its own property values.

Methods in Constructor Functions

To make object instances created with constructor functions do something, we can add methods to it.

We can define methods in constructor functions by writing:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function(){
   return `Hi ${this.name}`;
  }
}

In the code above, the this.greet property is a method of a Person instance. So if we create a new Person object with the constructor and call greet as follows …

let person = new Person('Joe', 10);
console.log(person.greet());

… then we get 'Hi Joe' from the console.log output.

Notice we used the function keyword to define the method. We have to use it to get the instance of the Person constructor as the value of this.

If we create another instance of the Person object, as follows …

let jane = new Person('Jane', 11);

… when we call thegreet method on jane as follows …

console.log(jane.greet());

… then we get 'Hi Jane’ from the console.log output.

Constructor-Mode Test

It’s easy to mistakenly call a constructor function without the new keyword. We can check if we used the new keyword to call the constructor function by using the new.target property.

For example, given we have the Person constructor …

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

… then we can log the new.target property, as follows:

function Person(name, age) {
  this.name = name;
  this.age = age;
  console.log(new.target);
}

Then, once we create a new Person with the new operator, as follows …

let person = new Person('Joe', 10);

… we get the code of the function logged, as follows:

ƒ Person(name, age) {
  this.name = name;
  this.age = age;
  console.log(new.target);
}

If we call the Person function without using the new operator, as follows …

Person('Joe', 10);

… we get undefined returned from the console.log.

Therefore, if we want to check if the constructor has been called with the new operator, we can check the new.target property.

Return From Constructor Functions

Just like regular functions, we can have a return statement in a constructor function.

However, unlike regular functions, there are some rules that control what gets returned in constructor functions.

If return is called with an object, then the object is returned instead of this. When we create a new object instance with the new operator, then we get the object returned instead of the value of this.

If return is called with a primitive object, then it’s ignored.

In all other cases, this is returned even without specifying anything.

We can return an object other than this by writing the following code:

function Person(name, age) {
  this.name = name;
  this.age = age;
  return {
    name: 'Mary'
  };
}

Then, when we create an instance of Person with the new keyword, as follows …

let person = new Person('Joe', 10);
console.log(person.name);

… we get 'Mary' logged.

Omitting Parentheses

We can omit parentheses when we invoke the constructor function if we don’t have to pass in any argument. For example, if we write …

function Employee() {
  this.name = "John";
}

let employee = new Employee;
console.log(employee.name);

… we get 'John' from the console.log output.

Checking if an Object Is an Instance of a Constructor Function

We can check if an object is an instance of a constructor function by using the instanceof operator.

For example, given we have the Person constructor and a person object created from it, as follows …

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function(){
   return `Hi ${this.name}`;
  }
}

let person = new Person('Joe', 10);

… we can use the instanceof operator as follows to check if person is created from the Person constructor:

console.log(person instanceof Person);

The console.log above should show true. It’ll be false if an object isn’t created from the constructor given.

For example, if we have …

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function(){
   return `Hi ${this.name}`;
  }
}

function Foo(){

}

let person = new Person('Joe', 10);
console.log(person instanceof Foo);

… then console.log shows false since person isn’t created from the Foo constructor.

Constructor functions are templates for creating objects. We can use it to create different objects using the same constructor, which has the same instance methods and properties with different values for the nonmethod properties.

We can use the instanceof operator to check if an object is created from a constructor.