Categories
TypeScript

Ways to Write Better JavaScript — Use TypeScript

The way we write JavaScript can always be improved. As the language evolves and more convenient features are added, we can also improve by using new features that are useful.

In this article, we’ll look at some ways to write better JavaScript by using TypeScript.

Use TypeScript

TypeScript is a natural extension to JavaScript. It lets us write JavaScript code that’s type-safe. Therefore, we can use it to prevent lots of data type errors that would otherwise occur if we didn’t use TypeScript.

Also, it provides autocomplete for things that otherwise wouldn’t have the autocomplete feature like many libraries. They use TypeScript type definitions to provide autocomplete for text editors and IDEs to make our lives easier.

TypeScript doesn’t turn JavaScript into a different language. All it does is add type checking to JavaScript by various type-checking features.

Therefore, all the knowledge that is used for JavaScript all apply to TypeScript.

For instance, we can create a function with TypeScript type annotations as follows:

const foo = (num: number): number => {
  return num + 1;
}

In the code above, we have the foo function with a num parameter that’s set to the type number . We also set the return type to number by specifying the type after the : .

Then if we call the function with a number, the TypeScript compiler will accept the code.

Otherwise, it’ll reject the code and won’t build the code. This is good because JavaScript doesn’t stop this from happening.

Interfaces

TypeScript provides us interfaces so that we know the structure of an object without logging the object or checking the value otherwise.

For instance, we can create one as follows:

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

Then we can use it as follows:

const person: Person = { name: 'jane', age: 10 }

If we miss any of these properties, then we’ll get an error as the TypeScript compiler is looking for them.

We can also use it to enforce a class implementation as follows:

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

class Person implements PersonInterface {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

In the code above, we have both the name and age fields. If we skip any of them, then we’ll get an error from the TypeScript compiler.

If we want to embrace the dynamic typing nature of JavaScript, we can add dynamic index signatures to JavaScript. Also, there’re union and intersection types to combine different types into one.

For instance, we can use it as follows:

interface PersonInterface {
    name: string;
    age: number;
    [key: string]: any;
}

In the code above, we have:

[key: string]: any;

to allow dynamic keys in anything that implements PersonInterface that has anything as a value.

Then we can have any property in addition to name and age in any class that implements PersonInterface or an object that’s cast to the PersonInterface type.

Union types let us join different types together. For instance, we can use it as follows:

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

interface Employee {
    employeeId: string;
}

const staff: Person | Employee = {
    name: 'jane',
    age: 10,
    employeeId: 'abc'
}

In the code above, the | is the union type operator. It lets us combine both the keys from both interfaces into one without creating a new type.

Another good thing about TypeScript is nullable properties. We can make properties optional with the ? operator.

For instance, we can use the following code:

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

With the ? operator, we made age an optional property.

typeof Operator

Another great feature of TypeScript is the typeof operator, which lets us specify that something has the same type as something else.

For instance, we can use it as follows:

const person = {
    name: 'jane',
    age: 10,
}

const person2: typeof person = {
    name: 'john',
    age: 11,
}

In the code above, we have the person2 object, which has the same type as person since we specified that with typeof person . Then person2 must have the name and age properties or we’ll get an error.

As we can see, we don’t need to specify any interfaces or classes explicitly to specify types. This is handy for getting the types of imported libraries that don’t come with type definitions.

Conclusion

With TypeScript, we made refactoring easy since it’s harder to break the existing code with the type and structure checks that it provides.

It also makes communication easier because we know the type and structure of our objects, classes, and return value of functions.

Categories
TypeScript

An Introduction to TypeScript Interfaces

The big advantage of TypeScript over plain JavaScript is that it extends the features of JavaScript by adding features that ensure type safety of our program’s objects.

It does this by checking the shape of the values that objects take on. Checking the shape is called duck typing or structural typing. Interfaces are one way to fill the role of naming data types in TypeScript.

They are very useful for defining a contract within our code in TypeScript programs.

In the last part, we’ll look at how to define a TypeScript interface and how to add properties to it. We also look at excess property checks for object literals and defining types for interfaces.

In this article, we’ll look at how to extend interfaces and write interfaces that extend classes.


Extending Interfaces

In TypeScript, interfaces can extend each other just like classes. This lets us copy the members of one interface to another and gives us more flexibility in how we use our interfaces.

We can reuse common implementations in different places and we can extend them in different ways without repeating code for interfaces.

We can extend interfaces with the extends keyword. We can use the keyword to extend one or more interfaces separated by commas. For example, we can use the extends keyword as in the code below:

interface AnimalInterface {
  name: string;  
}
interface DogInterface extends AnimalInterface {
  breed: string;
  age: number;
}
interface CatInterface extends AnimalInterface {
  breed: string;
}

Then, to implement the Dog and Cat interfaces, we have to implement the members listed in the Animal interface as well. For example, we would implement them as in the following code:

interface AnimalInterface {
  name: string;  
}
interface DogInterface extends AnimalInterface {
  breed: string;
  age: number;
}
interface CatInterface extends AnimalInterface {
  breed: string;
}
class Cat implements CatInterface {
  name: string = 'Mary';
  breed: string = 'Persian';
}
class Dog implements DogInterface {
  name: string = 'Jane';
  breed: string = 'Labrador';
  age: number = 10;
}

As we can see, we have added everything from the parent interface and the child interface in our class implementations. We can also extend multiple interfaces as in the following code:

interface MachineInterface {
  name: string;  
}
interface ProductInterface {
  price: number;
}
interface ClockInterface extends MachineInterface, ProductInterface {
  tick(): void;
}
class Clock implements ClockInterface {
  name: string = 'Quartz';
  price: number = 20;
  tick() {
    console.log('tick');
  }
}

As we can see from the code above, we have all the members of the Machineinterface, ProductInterface, and ClockInterface if we implement the ClockInterface as we did with the Clock class.

Note that if we have the same member name in multiple interfaces then they must have identical data types as well. For example, if we have the following code:

interface MachineInterface {
  name: string;  
}
interface ProductInterface {
  name: number;
}
interface ClockInterface extends MachineInterface, ProductInterface {
  tick(): void;
}
class Clock implements ClockInterface {
  name: string = 'Quartz';
  price: number = 20;
  tick() {
    console.log('tick');
  }
}

The Typescript compiler would reject it since we have name being a string in the MachineInterface and name being a number in the ProductInterface.

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

Interface ‘ClockInterface’ cannot simultaneously extend types ‘MachineInterface’ and ‘ProductInterface’. Named property ‘name’ of types ‘MachineInterface’ and ‘ProductInterface’ are not identical.(2320)

Hybrid Types

We can override the type that’s inferred by an object with the type assertion operator, which is denoted by the as keyword in TypeScript.

This way, we can use code that has dynamic types while we keep using the interface. For example, we can write the following code:

interface Person {
  name: string;
  (name: string): string;
}
function getPerson(): Person {
  let person = (function (name: string) { }) as Person;
  person.name = 'Joe';
  return person;
}
let person = getPerson();
person('Joe');

In the code above, we have the person variable in the getPerson function which we set explicitly with the Person type so that we can assign properties listed in the Person interface to the person variable.


Interfaces Extending Classes

TypeScript interfaces can extend classes. This means that an interface can inherit the members of a class but not their implementation. The class, in this case, acts as an interface with all the members declared without providing the implementation.

This means that when we extend a class with private or protected members, the interface can only be implemented by that class or a sub-class of it. For example, we can write an interface that extends a class as we do in the following code:

class Animal {
  name: string = '';
  private age: number = 0;  
}
interface BirdInterface extends Animal {
  breed: string;
  color: string;  
}
class Bird extends Animal implements BirdInterface {
  name: string = 'Bird';    
  breed: string = 'pigeon';
  color: string = 'Gray';
}

In the code above, we first created the class Animal which has a public member name and a private member age. Then, we added a BirdInterface which extends the Animal class by adding the public members breed and color.

Then, in the Bird class, which extends the Animal class and implements the BirdInterface, we have all the members of the BirdInterface plus the public members of the Animal class.

Since private members can’t be accessed outside of a class, we can’t access the member age in the Bird class. We also can’t add another age member in the Bird class.

Otherwise, we would get the errors:

Class ‘Bird’ incorrectly extends base class ‘Animal’. Property ‘age’ is private in type ‘Animal’ but not in type ‘Bird’.(2415)” and “Class ‘Bird’ incorrectly implements interface ‘BirdInterface’. Property ‘age’ is private in type ‘BirdInterface’ but not in type ‘Bird’.(2420)

However, if we change the age member in the Animal class to a protected member, which can be accessed by all sub-classes that extends Animal, then we can reference it in the Bird class as in the following code:

class Animal {
  name: string = '';
  protected age: number = 0;  
}
interface BirdInterface extends Animal {
  breed: string;
  color: string;    
}
class Bird extends Animal implements BirdInterface {
  name: string = 'Bird';    
  breed: string = 'pigeon';
  color: string = 'Gray';
  age: number = 1;
}

This is the same with methods. Private methods can’t be accessed by anything outside the class that it’s defined in and can’t be overridden by any sub-class or interface. For example, if we have the following code:

class Animal {
  name: string = '';
  private age: number = 0;  
  private getAge() {
    return this.age;
  }
}
interface BirdInterface extends Animal {
  breed: string;
  color: string;   
  getAge(): number;
}
class Bird extends Animal implements BirdInterface {
  name: string = 'Bird';    
  breed: string = 'pigeon';
  color: string = 'Gray';  
  getAge() { return 0 };
}

Then we would get the errors:

Class ‘Bird’ incorrectly extends base class ‘Animal’. Property ‘age’ is private in type ‘Animal’ but not in type ‘Bird’.(2415)” and “Class ‘Bird’ incorrectly implements interface ‘BirdInterface’. Property ‘age’ is private in type ‘BirdInterface’ but not in type ‘Bird’.(2420)

However, we can override protected methods in sub-classes as in the following code:

class Animal {
  name: string = '';
  private age: number = 0;  
  protected getAge() {
    return this.age;
  }
}
interface BirdInterface extends Animal {
  breed: string;
  color: string;     
}
class Bird extends Animal implements BirdInterface {
  name: string = 'Bird';    
  breed: string = 'pigeon';
  color: string = 'Gray';  
  getAge() { return 0 };
}

In TypeScript, interfaces can extend each other just like classes. This lets us copy the members of one interface to another and gives us more flexibility in how we use our interfaces.

We can reuse common implementations in different places and we can extend them in different ways without repeating code for interfaces.

Also, TypeScript interfaces can extend classes. This means that an interface can inherit the members of a class but not their implementation. The class, in this case, acts as an interface with all the members declared without providing the implementation.

This means that when we extend a class with private or protected members, the interface can only be implemented by that class or a sub-class of it.

Categories
TypeScript Best Practices

TypeScript Best Practices — Interfaces vs Type Aliases and Unnecessary Syntax

To make code easy to read and maintain, we should follow some best practices.

In this article, we’ll look at some best practices we should follow to make everyone’s lives easier.

Class Name Casing

TypeScript class names should be PascalCase.

This also applies to interfaces.

So we write:

class Foobar {}

and:

interface FooBar {}

Use UTF-8 Encoding for Files

We should use UTF-8 encoding for files.

This way, there won’t be any issues using the file anywhere.

File Name Casing

We should name our files with a consistent case.

We can stick with camel case, Pascal case, kebab case, or snake case.

Camel case is fileName.ts .

Pascal case is FileName.ts ,

Kebab case is file-name.ts .

Snake case is file_name.ts .

We just stick with one for all files.

Use Explicit Increment or Decrement Operators

We should use explicit increment or decrement operators to make sure that we just assign the new value and don’t use the return value.

For instance, instead of writing:

++i;
i++;
--j;
j--;

which return the value and do the increment or decrement operation, we write:

i += 1;
i -= 1;
j += 1;
j -= 1;

Interface

We may want to have interfaces that start with I to distinguish them from other entities.

For instance, we may want to write:

interface IFoo {
  bar: string;
}

Use Interface Over Type Literal

Interfaces can be implemented, extended, and merged, so they’re preferred to type literals.

For instance, instead of writing:

type Alias = {
  num: number
}

We write:

interface Foo {
  num: number;
}

Match Default Export Name

If we have a default export, then our default import should match the name of the export.

This reduces confusion.

So if we have:

bar.ts

export default foo;

Then we write:

import foo from bar;

Newline Per Chained Call

If we have a chain of method calls, then we may want to put each of them in a new line so that it won’t overflow the page.

For instance, instead of writing:

foo.bar().baz();

We write:

foo
  .bar()
  .baz();

No Angle Bracket Type Assertion

We should use as for type assertions since we can use it for type assertions in .tsx files in addition to .ts files.

So instead of writing:

const foo = <Bar>bar;

We write:

const foo = bar as Bar;

No Boolean Literal Comparison

We shouldn’t compare with boolean literals since they’re redundant.

For instance, we can shorten:

if (x === true)

to:

if (x)

Use of Parameter Properties

We can have parameter properties in the TypeScript code.

They let us pass in constructor parameter and assign it to a value at the same time.

Instead of writing:

class Animal {
  constructor(private numLegs: number) {}
}

We can write:

class Animal {
  private numLegs: number = 2;
  constructor(numLegs: number) {
    this.numLegs = numLegs;
  }
}

No Reference Import

We shouldn’t use reference if we use import .

For instance, if we have:

<reference path="foo.bar" />

We write:

import { bar } from 'foo';

It’s more standard and we don’t need reference to pull type definitions out of type definition files anymore.

No Useless Callback Wrappers

We shouldn’t have useless callback wrappers in our code.

For instance, instead of writing:

const handleContent = (content) => console.log('do something with', content);

promise.then((content) => handleContent(content))

We write:

const handleContent = (content) => console.log('do something with', content);

promise.then(handleContent)

It’s a lot cleaner.

No Unnecessary Initializers

We shouldn’t have to set variables to undefined .

For instance, instead of writing:

let x =  undefined;

We write:

let x;

No Object Literal Key Quotes

If an object literal has valid identifiers as property names, then we don’t need quotes around the names.

For instance, instead of writing:

const obj = { 'foo': 1 };

We write:

const obj = { foo: 1 };

Conclusion

We should have a consistent file name casing.

Also, we shouldn’t have unnecessary syntax in our code.

Imports should be used to pull data type definitions instead from external sources.

Useless callback wrappers should also be removed.

Interfaces should be used instead of type alias since they’re more versatile.

Categories
TypeScript Best Practices

TypeScript Best Practices — Namespace, any, for-in and More

To make code easy to read and maintain, we should follow some best practices.

In this article, we’ll look at some best practices we should follow to make everyone’s lives easier.

Group Function Overloads Together

We can overload functions.

This means we can have multiple function signatures for a function, which TypeScript can check for.

To make our lives easier, we should group them together so that it’s easier to read:

function foo(a: string, b: string): string;

function foo(a: number, b: number): number;

function foo(a: any, b: any): any {
  console.log(a, b);
}

Visibility Declarations for Class Members

To take advantage of the access control capabilities of TypeScript, we can add the visibility declarations or class members.

For instance, we write:

class Employee {
  private getSalary(): number {
    return 90000;
  }
}

We added the private access modifier so that getSalary can only be called by other methods in the class.

There’s also the public modifier to make the member available to outside code.

protected makes the member available to subclasses and the current class.

public is the default.

We can also do the same for instance variables:

class Employee {
  private empCode: number;
}

Ordering Class Members

We may consider ordering class members to make the members easier to read.

We can order them by access modifiers, alphabetical order, etc.

It’s good to stick with one.

For instance, instead of writing:

class Employee {
  private id: string;
  private empCode: number;
  private empName: string;
}

We write:

class Employee {
  private empCode: number;
  private empName: string;
  private id: string;
}

to sort them by alphabetical order.

Eliminate the Use of any Types

We can eliminate the use of any types in our code to take advantage of TypeScript’s type-checking capabilities.

If we need something more flexible than static types, there are many ways to define them.

We can use literal types to restrict values to only some literals.

There’re union types to let us check for members for multiple types.

The intersection type makes sure that the variable has members that are in both types.

Index signatures let us check for dynamic properties.

There’re many ways to avoid any .

For instance, instead of writing:

let bar: any;

We write:

let foo: string;

No Empty Interface

We shouldn’t have empty interfaces in our code since they’re useless.

So instead of writing:

interface I {}

We write:

interface I {
  bar: number;
}

No for-in Loops

for-in loops are legacy JavaScript syntax which has better modern alternatives.

It’s bad since we need to use the hasOwnProperty to check for non-inherited properties with it.

Better alternatives include Object.keys to get the non-inherited keys of an object.

Object.values to get the values and Object.entries to get all entries.

So instead of writing:

for (const key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(obj[key]);
  }
}

We write:

for (const key in Oject.keys(obj)) {
  console.log(obj[key]);
}

No Import Side Effects

import should be used for import module members and using them.

If they perform side effects, then it’s not good because it’s hard to statically analyze the code.

So instead of writing:

import 'foo';

We write:

import { bar } from 'foo';

Some exceptions may be importing CSS with Webpack as modules.

So we may still write:

import 'styles.css';

No Explicit Type Declarations for Variable or Parameters with Literal Values

It’s redundant to have type declarations for variables or parameters that are assigned with numbers strings or boolean.

TypeScript can check these without an explicit type.

Therefore, instead of writing:

const foo: number = 10;

We write:

const foo = 10;

Don’t Use module Keyword for Namespaces

If we declare namespaces, then we should use the namespace keyword.

Instead of writing:

module Math {
  function add(a: number, b: number): number {
    return a + b;
  }
}

We write:

namespace Math {
  function add(a: number, b: number): number {
    return a + b;
  }
}

This way, we won’t confuse what we have with ES modules.

Conclusion

Grouping things together make them easier to read.

Visibility modifiers are a useful TypeScript feature.

any types can be replaced with many things.

Use namespace to declare namespaced code.

Categories
TypeScript Best Practices

TypeScript Best Practices — Function and Array Types, Reducing Complexity

To make code easy to read and maintain, we should follow some best practices.

In this article, we’ll look at some best practices we should follow to make everyone’s lives easier.

Use isNaN to Check for NaN

NaN doesn’t equal itself, so we can’t use === or !== to check for NaN .

Instead, we use the isNaN function to check for it.

So instead of writing:

if (foo === NaN) {}

We write:

if (isNaN(foo)) {}

Invalid Use of void

We should watch for invalid uses of void .

We shouldn’t mix void with other types since void means nothing rather than an explicit type of value.

If we need undefined , then we should use the undefined type.

So types expressions like:

Foo | void

should be replaced with:

Foo | undefined

Max Number of Classes Per File

We shouldn’t have too many classes per file.

It’s probably too complex if we do.

We can set the max number of classes per file with a linter.

Max File Line Count

If a file has too many lines, then it’s probably too complex and hard to read.

We can set the max file line count in a file with a linter.

Default Exports

Default exports aren’t as clear, so we should avoid using them.

Named exports are clearer.

So we may want to avoid them.

Default Import

Like default exports, default imports aren’t as clear as named imports.

No Duplicate Imports

We can combine multiple import statements from the same module into one import statement, so we should do that.

For instance, instead of writing:

import { foo } from 'module';
import { bar } from 'module';

We write:

import { foo, bar } from 'module';

No Mergeable Namespace

We shouldn’t have 2 namespaces with the same name so we don’t have to think about how they’ll be merged together.

Instead, we can just name different namespaces with different names.

For instance, instead of writing:

namespace Animals {
  export class Dog {}
}

namespace Animals {
  export interface Cat {
    numberOfLegs: number;
  }
  export class Zebra {}
}

We write:

namespace Pets {
  export class Dog {}
  export interface Cat {
    numberOfLegs: number;
  }
}

name WildAnimals {
  export class Zebra {}
}

No require Imports

Now that ES modules are widely available, we can replace require with import .

For instance, instead of writing:

const { bar } = require('foo');

We can use the ES6 module version of the module and write:

import { bar } from 'foo';

Use const

We should use const instead of let or var whenever we can for declaring variables.

This way, they can’t be accidentally be assigned with another value.

For instance, instead of writing:

var x = 1;
let y = 2;

We write:

const z = 3;

Marking Variables as readonly

We should mark variables as readonly if they’re private and are never modified outside of the constructor.

This way, we won’t be able to accidentally change them.

For instance, instead of writing:

class Foo {
  private bar = 1;
}

We write:

class Foo {
  private readonly bar = 1;
}

Arrow return Shorthand

If we have an arrow function that only has one statement and returns something, we can make them shorter.

Instead of writing:

() => { return x; }

We write:

() => x

Set Array Types

We should set the types of the array content so that we can restrict the types of values that go into the array.

We can also set generic types to make the array generic.

For instance, instead of writing:

const arr = ['foo', 'bar', 1];

We write:

const arr: string[] = ['foo', 'bar', 'baz'];

Callable Types

If we have an interface or a literal type with just a call signature, then we should write it as a function type.

For instance, instead of writing:

interface SearchFunc {
  (source: string, subString: string): boolean;
}

We write:

(source: string, subString: string) => boolean

Conclusion

We should use function types instead of interfaces or literal types with just a function signature.

Arrays should have a type so we can’t put any type of data in it.

We should reduce the complexity of each file.

isNaN should be used for checking for NaN .