Categories
TypeScript Best Practices

TypeScript Antipatterns to Avoid

TypeScript is a language that extends the capabilities of JavaScript by adding type annotations to JavaScript code. This lets us avoid bugs from unexpected data types.

In this article, we’ll look at some antipatterns to avoid when writing TypeScript code.

Overusing the any Type

The point of using TypeScript is to have types in variables and functions. So we should use them wherever we can.

Therefore, we shouldn’t use the any type in most of our code.

Overusing Classes

If our TypeScript classes don’t have many methods, then we don’t need to define a class just to use it to type variables and functions.

Also, if we only have a single instance, then wrapping the logic within a class doesn’t make sense.

Instantiating classes introduce complex and it’s hard to optimize when minifying code.

Instead, we can define object literals and use interfaces or the typeof operator to get the type of an object.

For example, write the following interface for objects:

interface Person {
    firstName: string;
    lastName: string;
}

const person: Person = {
    firstName: 'Jane',
    lastName: 'Smith'
}

Then we can use them like we have above with person .

Also, if we don’t know the exact structure that the object will have, we can use typeof operator as follows:

const person = {
    firstName: 'Jane',
    lastName: 'Smith'
}

const person2: typeof person = {
    firstName: 'Joe',
    lastName: 'Smith'
}

Using typeof is a convenient way to define types without writing lots of code.

If our object has methods, we can do the same thing as above:

interface Person {
    firstName: string;
    lastName: string;
    fullName: (firstName: string, lastName: string) => string
}

const person: Person = {
    firstName: 'Jane',
    lastName: 'Smith',
    fullName(firstName: string, lastName: string) {
        return `${firstName} ${lastName}`;
    }
}

TypeScript will do type inference with typeof :

const person = {
    firstName: 'Jane',
    lastName: 'Smith',
    fullName(firstName: string, lastName: string) {
        return `${firstName} ${lastName}`;
    }
}
const person2: typeof person = {
    firstName: 'Joe',
    lastName: 'Smith',
    fullName(firstName, lastName) {
        return `${firstName} ${lastName}`;
    }
}

It’ll force us to add the fullName method to person2 if it’s missing, and it’ll force us to add the parameters and return a string.

Otherwise, we’ll get compiler errors.

Using the Function Type

The Function type is a generic type for functions. It’s like any for variables. We should specify the data types of parameters and return types in our functions.

Instead, we should add types to parameters and return types as follows:

type ArithmeticFn = (a: number, b: number) => number
const add: ArithmeticFn = (a: number, b: number): number => a + b;

We have the types in the type alias ArithmeticFn and also add . That’s enough type annotations in our code.

Also, once again, we can use typeof to do type inference:

const add = (a: number, b: number): number => a + b;
const subtract: typeof add = (a, b) => a-b

Then subtract also has the same parameter and return types as add .

Messing with Type Inference

We shouldn’t put useless type annotations with type inference will do the job.

For example, in the following example:

const courses = [{
    name: 'Intro to TypeScript'
}]
const [course] = courses;
const newCourse: any = {...course};
newCourse.description = 'Great intro to TypeScript';
courses[0] = newCourse;

We have an extra any annotation that we shouldn’t have.

Instead, if we want to add a new property to an object after copying it, we can write the following:

const courses = [{
    name: 'Intro to TypeScript'
}]
const [course] = courses;
const newCourse = {...course, description: 'Great intro to TypeScript'};
courses[0] = newCourse;

Then the TypeScript compiler won’t throw an error and we still get type inference from the TypeScript compiler since we didn’t use the any type.

Copying and Pasting Partial Type Definitions

Another thing that we shouldn’t do is copy and pasting partial type definitions from other places into our own code.

If we want to get the type of an object with an unknown type, we can use the typeof operator.

For example, we can write:

const person = {
    firstName: 'Joe',
    lastName: 'Smith',
    age: 20
}

const person2: typeof person = {
    firstName: 'Jane',
    lastName: 'Smith',
    age: 20
}

The TypeScript compiler will automatically recognize the type of person and will do checks when we define person2 to check if everything’s there.

Lookup Type

We can also use one property of an object as its own type. This is called a lookup type.

For example, we can write:

const person = {
    firstName: 'Joe',
    lastName: 'Smith',
    age: 20
}

const foo: typeof person.firstName = 'foo';

In the code above, TypeScript recognized person.firstName ‘s type is a string, so typeof person.firstName would be a string.

Mapped Types

We can create mapped types to map all properties of a type to something else.

For example, we can make all the properties of a type optional by writing:

interface Person{
    firstName: string;
    lastName: string;
}

type Optional<T> = {
    [P in keyof T]?: T[P];
};

const partialPerson: Optional<Person> = {};

The code above compiles and runs because we created a new type from Person where all the properties are optional.

Getting the Return Type of a Function

We can get the return type function with the ReturnType generic to pass in a type of the function. Then we’ll get the return type of that function.

For example, if we have:

const add = (a: number, b: number) => a + b;
const num: ReturnType<typeof add> = 1;

Then ReturnType<typeof add> will be number , so we have to assign a number to it.

TypeScript’s type inference is also working here. The ReturnType generic is available since TypeScript 2.8

Conclusion

In many cases, we can use the typeof operator, lookup types, or mapped types to annotate the types flexibly without losing type check capabilities.

This means we should eliminate the use of any as much as possible.

Also, we don’t need classes just for adding types to a few objects.

Categories
Vuetify

Vuetify — Tabs

Vuetify is a popular UI framework for Vue apps.

In this article, we’ll look at how to work with the Vuetify framework.

Tabs

We can add tabs to our Vuetify app with the v-tabs component.

For example, we can write:

<template>
  <v-tabs fixed-tabs background-color="indigo" dark>
    <v-tab>One</v-tab>
    <v-tab>Two</v-tab>
    <v-tab>Three</v-tab>
  </v-tabs>
</template>
<script>
export default {
  name: "HelloWorld",
};
</script>

to show tabs.

background-color has the background color.

fixed-tabs makes its position fixed.

dark makes the text of the non-active tabs gray.

The active tab is always centered.

Tab Items

We can add the v-tab-items component to let us customize the content per tab.

For example, we can write:

<template>
  <v-card>
    <v-tabs v-model="tab" background-color="primary" dark>
      <v-tab v-for="item in items" :key="item.tab">{{ item.tab }}</v-tab>
    </v-tabs>

    <v-tabs-items v-model="tab">
      <v-tab-item v-for="item in items" :key="item.tab">
        <v-card flat>
          <v-card-text>{{ item.content }}</v-card-text>
        </v-card>
      </v-tab-item>
    </v-tabs-items>
  </v-card>
</template>
<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      tab: null,
      items: [
        { tab: "One", content: "Tab 1 Content" },
        { tab: "Two", content: "Tab 2 Content" },
        { tab: "Three", content: "Tab 3 Content" },
        { tab: "Four", content: "Tab 4 Content" },
        { tab: "Five", content: "Tab 5 Content" },
      ],
    };
  },
};
</script>

to add tabs and populate the headings and content.

v-tab has the tab headings and v-tab-items has the tab items.

The tab state has the index of the active tab.

v-model lets us control which tab is active programmatically.

We just render all the tab content with the v-tab-item component and the right one will be displayed automatically.

Grow Tabs

We can add the grow prop to make the tag items take up all available space up to a max-width of 300px:

<template>
  <v-card>
    <v-tabs v-model="tab" background-color="primary" grow dark>
      <v-tab v-for="item in items" :key="item.tab">{{ item.tab }}</v-tab>
    </v-tabs>

    <v-tabs-items v-model="tab">
      <v-tab-item v-for="item in items" :key="item.tab">
        <v-card flat>
          <v-card-text>{{ item.content }}</v-card-text>
        </v-card>
      </v-tab-item>
    </v-tabs-items>
  </v-card>
</template>
<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      tab: null,
      items: [
        { tab: "One", content: "Tab 1 Content" },
        { tab: "Two", content: "Tab 2 Content" },
        { tab: "Three", content: "Tab 3 Content" },
        { tab: "Four", content: "Tab 4 Content" },
        { tab: "Five", content: "Tab 5 Content" },
      ],
    };
  },
};
</script>

We have the grow prop to make it grow to fix the screen.

Pagination

The show-arrows prop will make the arrow display so that we can scroll through the tabs:

<template>
  <v-card>
    <v-tabs v-model="tab" background-color="primary" show-arrows dark>
      <v-tab v-for="item in items" :key="item.tab">{{ item.tab }}</v-tab>
    </v-tabs>

    <v-tabs-items v-model="tab">
      <v-tab-item v-for="item in items" :key="item.tab">
        <v-card flat>
          <v-card-text>{{ item.content }}</v-card-text>
        </v-card>
      </v-tab-item>
    </v-tabs-items>
  </v-card>
</template>
<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      tab: null,
      items: [
        { tab: "One", content: "Tab 1 Content" },
        { tab: "Two", content: "Tab 2 Content" },
        { tab: "Three", content: "Tab 3 Content" },
        { tab: "Four", content: "Tab 4 Content" },
        { tab: "Five", content: "Tab 5 Content" },
      ],
    };
  },
};
</script>

The arrows will show when the tabs overflow the page.

Conclusion

We can add tabs with the v-tabs and tab content with the v-tab-item component.

Categories
Vuetify

Vuetify — Table Sorting and Pagination

Vuetify is a popular UI framework for Vue apps.

In this article, we’ll look at how to work with the Vuetify framework.

External Sorting

We can control sorting externally with various props.

For instance, we can write:

<template>
  <div>
    <v-data-table
      :headers="headers"
      :items="desserts"
      :sort-by.sync="sortBy"
      :sort-desc.sync="sortDesc"
      class="elevation-1"
    ></v-data-table>
    <div class="text-center pt-2">
      <v-btn color="primary" class="mr-2" @click="toggleOrder">Toggle sort order</v-btn>
      <v-btn color="primary" @click="nextSort">Sort next column</v-btn>
    </div>
  </div>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    sortBy: "fat",
    sortDesc: false,
    headers: [
      {
        text: "Dessert (100g serving)",
        align: "start",
        value: "name",
      },
      { text: "Calories", value: "calories" },
      { text: "Fat (g)", value: "fat" },
    ],
    desserts: [
      {
        name: "Frozen Yogurt",
        calories: 159,
        fat: 6.0,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
        fat: 9.0,
      },
      {
        name: "Eclair",
        calories: 262,
        fat: 16.0,
      },
    ],
  }),
  methods: {
    toggleOrder() {
      this.sortDesc = !this.sortDesc;
    },
    nextSort() {
      let index = this.headers.findIndex((h) => h.value === this.sortBy);
      index = (index + 1) % this.headers.length;
      this.sortBy = this.headers[index].value;
    },
  },
};
</script>

We change the sortBy and sortDesc prop values to let us change the column to sort by and whether we sort ascending or descending respectively.

The sync modifier is required for updating the table properly when the buttons are clicked to change the prop values.

Paginate and Sort Server-Side

We can do the pagination and sorting on the server-side.

For example, we can write:

<template>
  <div>
    <v-data-table
      :headers="headers"
      :items="desserts"
      :options.sync="options"
      :server-items-length="totalDesserts"
      :loading="loading"
      class="elevation-1"
    ></v-data-table>
  </div>
</template>
<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      totalDesserts: 0,
      desserts: [],
      loading: true,
      options: {},
      headers: [
        {
          text: "Dessert (100g serving)",
          align: "start",
          sortable: false,
          value: "name",
        },
        { text: "Calories", value: "calories" },
        { text: "Fat (g)", value: "fat" },
      ],
    };
  },
  watch: {
    options: {
      async handler() {
        const { items, total } = await this.getDataFromApi();
        this.desserts = items;
        this.totalDesserts = total;
      },
      deep: true,
    },
  },
  async mounted() {
    const { items, total } = await this.getDataFromApi();
    this.desserts = items;
    this.totalDesserts = total;
  },
  methods: {
    getDataFromApi() {
      this.loading = true;
      return new Promise((resolve, reject) => {
        const { sortBy, sortDesc, page, itemsPerPage } = this.options;

        let items = this.getDesserts();
        const total = items.length;

        if (sortBy.length === 1 && sortDesc.length === 1) {
          items = items.sort((a, b) => {
            const sortA = a[sortBy[0]];
            const sortB = b[sortBy[0]];

            if (sortDesc[0]) {
              if (sortA < sortB) return 1;
              if (sortA > sortB) return -1;
              return 0;
            } else {
              if (sortA < sortB) return -1;
              if (sortA > sortB) return 1;
              return 0;
            }
          });
        }

        if (itemsPerPage > 0) {
          items = items.slice((page - 1) * itemsPerPage, page * itemsPerPage);
        }

        setTimeout(() => {
          this.loading = false;
          resolve({
            items,
            total,
          });
        }, 1000);
      });
    },
    getDesserts() {
      return [
        {
          name: "Frozen Yogurt",
          calories: 159,
          fat: 6.0,
        },
        {
          name: "Ice cream sandwich",
          calories: 237,
          fat: 9.0,
        },
        {
          name: "Eclair",
          calories: 262,
          fat: 16.0,
        },
      ];
    },
  },
};
</script>

The getDataFromApi method returns a promise that resolves to the data that we want to populate in the table.

The sorting is done with the sort method in the promise.

In the mounted hook, we get the data and set it.

this.desserts has the items.

totalDesserts is a number with the total number of desserts.

We set totalDessert as the value of server-items-length .

And items has the desserts array as its value.

Conclusion

We can sort and paginate data from the client or server-side with Vuetify.

Categories
Vuetify

Vuetify — Table Slots and Pagination

Vuetify is a popular UI framework for Vue apps.

In this article, we’ll look at how to work with the Vuetify framework.

Customizing Default Header

We can customize the default header with the header.<name> slot.

<name> is the name of the value property in the header item sent to headers .

For example, we can write:

<template>
  <v-data-table :headers="headers" :items="desserts" class="elevation-1">
    <template v-slot:header.name="{ header }">{{ header.text.toUpperCase() }}</template>
  </v-data-table>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    headers: [
      {
        text: "Dessert (100g serving)",
        align: "start",
        value: "name",
      },
      { text: "Calories", value: "calories" },
      { text: "Fat (g)", value: "fat" },
    ],
    desserts: [
      {
        name: "Frozen Yogurt",
        calories: 159,
        fat: 6.0,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
        fat: 9.0,
      },
      {
        name: "Eclair",
        calories: 262,
        fat: 16.0,
      },
    ],
  }),
};
</script>

to add a table with our own headings.

We populate the header.name slot to customize the display of the name column heading.

We display it as upper case.

Customizing Columns

We can customize some columns with the item.<name> slot.

<name> is the property name of the column we want to change.

For example, we can write:

<template>
  <v-data-table :headers="headers" :items="desserts" class="elevation-1">
    <template v-slot:item.calories="{ item }">
      <v-chip :color="getColor(item.calories)" dark>{{ item.calories }}</v-chip>
    </template>
  </v-data-table>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    headers: [
      {
        text: "Dessert (100g serving)",
        align: "start",
        value: "name",
      },
      { text: "Calories", value: "calories" },
      { text: "Fat (g)", value: "fat" },
    ],
    desserts: [
      {
        name: "Frozen Yogurt",
        calories: 159,
        fat: 6.0,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
        fat: 9.0,
      },
      {
        name: "Eclair",
        calories: 262,
        fat: 16.0,
      },
    ],
  }),
  methods: {
    getColor(calories) {
      if (calories > 400) return "red";
      else if (calories > 200) return "orange";
      else return "green";
    },
  },
};
</script>

We add a chip to the item.calories slot to change how the calories column is displayed.

External Pagination

We can add external pagination with the options prop.

For instance, we can write:

<template>
  <div>
    <v-data-table
      :headers="headers"
      :items="desserts"
      :page.sync="page"
      :items-per-page="itemsPerPage"
      hide-default-footer
      class="elevation-1"
      [@page](http://twitter.com/page "Twitter profile for @page")-count="pageCount = $event"
    ></v-data-table>
    <div class="text-center pt-2">
      <v-pagination v-model="page" :length="pageCount"></v-pagination>
      <v-text-field
        :value="itemsPerPage"
        label="Items per page"
        type="number"
        min="-1"
        max="15"
        [@input](http://twitter.com/input "Twitter profile for @input")="itemsPerPage = parseInt($event, 10)"
      ></v-text-field>
    </div>
  </div>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    page: 1,
    pageCount: 0,
    itemsPerPage: 10,
    headers: [
      {
        text: "Dessert (100g serving)",
        align: "start",
        value: "name",
      },
      { text: "Calories", value: "calories" },
      { text: "Fat (g)", value: "fat" },
    ],
    desserts: [
      {
        name: "Frozen Yogurt",
        calories: 159,
        fat: 6.0,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
        fat: 9.0,
      },
      {
        name: "Eclair",
        calories: 262,
        fat: 16.0,
      },
    ],
  }),
  methods: {
    getColor(calories) {
      if (calories > 400) return "red";
      else if (calories > 200) return "orange";
      else return "green";
    },
  },
};
</script>

to add a text field to our page to let users change the items per page.

Also, we added the v-pagination component to add the pagination buttons.

Conclusion

We can change how columns and pagination buttons are displayed with various components and slots.

Categories
Vuetify

Vuetify — Tables

Vuetify is a popular UI framework for Vue apps.

In this article, we’ll look at how to work with the Vuetify framework.

Tables

We can add a table with the v-simple-table component.

For example, we can add it by writing:

<template>
  <v-simple-table height="300px">
    <template v-slot:default>
      <thead>
        <tr>
          <th class="text-left">Name</th>
          <th class="text-left">Calories</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in desserts" :key="item.name">
          <td>{{ item.name }}</td>
          <td>{{ item.calories }}</td>
        </tr>
      </tbody>
    </template>
  </v-simple-table>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    desserts: [
      {
        name: "Yogurt",
        calories: 159,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
      },
      {
        name: "Eclair",
        calories: 262,
      }
    ],
  }),
};
</script>

We have the v-simple-table component with the default slot populated with the table rows.

We just use v-for to render the array entries into rows.

Fixed Header

The fixed-header with the height prop together let us fix the header to the top of the table.

For example, we can write:

<template>
  <v-simple-table height="150px" fixed-header>
    <template v-slot:default>
      <thead>
        <tr>
          <th class="text-left">Name</th>
          <th class="text-left">Calories</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in desserts" :key="item.name">
          <td>{{ item.name }}</td>
          <td>{{ item.calories }}</td>
        </tr>
      </tbody>
    </template>
  </v-simple-table>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    desserts: [
      {
        name: "Yogurt",
        calories: 159,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
      },
      {
        name: "Eclair",
        calories: 262,
      },
    ],
  }),
};
</script>

to make the table scrollable and keep the header always on top.

Dense Table

To add a dense version of the table, we can use the dense prop.

For example, we can write:

<template>
  <v-simple-table dense>
    <template v-slot:default>
      <thead>
        <tr>
          <th class="text-left">Name</th>
          <th class="text-left">Calories</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in desserts" :key="item.name">
          <td>{{ item.name }}</td>
          <td>{{ item.calories }}</td>
        </tr>
      </tbody>
    </template>
  </v-simple-table>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    desserts: [
      {
        name: "Yogurt",
        calories: 159,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
      },
      {
        name: "Eclair",
        calories: 262,
      },
    ],
  }),
};
</script>

The rows will be shorter than the default version.

Dark Theme

The dark prop lets us switch the table to the dark theme:

<template>
  <v-simple-table dark>
    <template v-slot:default>
      <thead>
        <tr>
          <th class="text-left">Name</th>
          <th class="text-left">Calories</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in desserts" :key="item.name">
          <td>{{ item.name }}</td>
          <td>{{ item.calories }}</td>
        </tr>
      </tbody>
    </template>
  </v-simple-table>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    desserts: [
      {
        name: "Yogurt",
        calories: 159,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
      },
      {
        name: "Eclair",
        calories: 262,
      },
    ],
  }),
};
</script>

Now the table will have a black background.

Conclusion

We can add a simple table with Vuetify’s v-simple-table component.