Categories
TypeScript

Using TypeScript — Arrays

TypeScript is a natural extension of JavaScript that’s used in many projects in place of JavaScript.

However, not everyone knows how it actually works.

In this article, we’ll look at how to define and use arrays in our TypeScript code.

Working with Arrays

JavaScript arrays can contain data from any combination of types and have variable lengths.

Items can be added or removed without the needed to resize the array explicitly.

TypeScript doesn’t change the flexible array sizing, but we can use it to restrict the types of data that can be in an array.

For instance, we can write:

let prices: number[] = [48, 23, 41];

to restrict the prices array to only hold numbers.

number is the data type of each entry and [] indicates that it’s an array.

We can also use parentheses to indicate that an array can hold entries with different types of data.

For instance, we can write:

let prices: (number | string)[] = [48, 23, '88'];

So that prices can have strings in addition to numbers.

TypeScript will ensure that only operations that are allowed for number values are performed by the function.

Equivalent Array Syntax

We can specify the array data type with brackets.

For instance, we can write:

let prices: Array<number> = [48, 23, 88];

which is the same as:

let prices: number[] = [48, 23, 88];

Inferred Typing for Arrays

If the data type is obvious from the values, then we don’t have to write the data type annotation explicitly.

For instance, we can write:

let prices = [48, 23, 88];

The TypeScript compiler will know that we have a number array just from the assignment.

We can see that if we loop through the array with forEach and try to do something with each entry:

let prices: number[] = [48, 23, 88];

prices.forEach(p => {
  console.log(p.toFixed(2));
});

The compiler lets us call the toFixed method on an array, which is a method available for numbers.

It’s great at inferring type from existing values.

Problems with Inferred Array Types

We may run into problems if we have a possibility of type mismatches.

For instance, we can write:

const prices: number[] = [48, 23, 88];
const taxes: number[] = [];

const getTax = (price: number, format: boolean) => {
  if (format) {
    return (price * 0.2).toFixed(2);
  }
  return price * 0.2;
};

prices.forEach(p => {
  taxes.push(getTax(p, false));
});

We’ll get the error:

Argument of type 'string | number' is not assignable to parameter of type 'number'.

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

from the compiler since getTax may return a number or a string.

Even if we pass in false , which should return a number, the compiler isn’t smart enough to figure that out on its own.

Therefore, we can either set the type of taxes to be number | string .

Or we can assert that each entry is a number.

Empty Arrays

If we have an empty array, the type would be inferred as any[] implicitly.

To avoid this, we should specify the type of it explicitly.

This applies if strictNullChecks is set to false .

never Array Type

When null and undefined aren’t assignable to other types, Typescript infers to empty arrays differently.

It’ll have the never type instead of any[] if strictNullChecks is set to true .

Inferring the type as never ensures that the array doesn’t escape any type checking process and the code won’t compile until the type of each entry is asserted or the array is initialized to the values that let the compiler infer the type.

Conclusion

TypeScript can infer the types of arrays if entries are added explicitly.

Inferred array types are different for empty arrays depending if strictNullChecks is true or not in the compiler options.

We can also set the type of arrays explicitly.

The TypeScript compiler isn’t smart enough to infer all data types on its own.

So data type assertions may be needed.

Categories
TypeScript

Using TypeScript — Function Return Values and Overloads

TypeScript is a natural extension of JavaScript that’s used in many projects in place of JavaScript.

However, not everyone knows how it actually works.

In this article, we’ll look at how to control function return values and overloads.

Disabling Implicit Returns

JavaScript is very flexible with its return types.

JavaScript functions return undefined if the function doesn’t have an implicit return statement.

This is known as the implicit return feature.

To prevent implicit returns, we can set the noImplicitReturns in compilerOptions to true .

This way, if there are paths through functions that don’t explicitly produce a result with the result keyword, an error would be thrown.

If we disable implicit returns, then we’ve to be explicit about what we return.

For instance, if we have a parameter that can be null :

const getTax = (price: number | null, ...fees: number[]): number => {
  return price * 0.2 - fees.reduce((total, fee) => total + fee, 0);
};

Then we’ll get a warning about price being possibly null from the compiler.

To fix the error, we’ve to make the returns explicit.

For instance, we can write:

const getTax = (price: number | null, ...fees: number[]) => {
  if (price !== null) {
    return price * 0.2 - fees.reduce((total, fee) => total + fee, 0);
  }
  return undefined;
};

We added a null check for price and return undefined is it’s null .

Void Functions

Functions that don’t return anything have a void return type.

For instance, we can define a void function by writing:

const greet = (): void => {
  console.log("hello");
};

Since our function returns nothing, we use void to denote that.

Overloading Function Types

With TypeScript, we can overload functions.

This means that we can define multiple functions for the function with the same name.

For instance, we can write:

function getTax(price: number): number;
function getTax(price: null): number;
function getTax(price: number | null): number {
  if (price !== null) {
    return price * 0.2;
  }
  return 0;
}

Now we can take a number parameter or null parameter when we call getTax .

The overloads only provide information about the various signatures to the TypeScript compiler.

It will be combined into one function in the built JavaScript code that we actually run.

Conclusion

With TypeScript, just like JavaScript, implicit returns undefined if no return statement is specified explicitly.

We can make the TypeScript compiler avoid implicit returns with the noImplicitReturns option set to true .

TypeScript lets us define multiple function signatures for the same function.

This let us accept different kinds of data within one function and express that in a clear way.

Categories
TypeScript

Using TypeScript — Class Inheritance and Interfaces

TypeScript is a natural extension of JavaScript that’s used in many projects in place of JavaScript.

However, not everyone knows how it actually works.

In this article, we’ll look at how to work with classes and interfaces in TypeScript.

Class Inheritance

TypeScript builds on the standard clas inheritance features to make them more familiar to developers using other programming languages.

It keeps the extends keyword like JavaScript for creating subclasses.

For instance, we can write:

class Person {
  constructor(public name: string) {}
}

class Student extends Person {
  constructor(readonly id: number, public name: string) {
    super(name);
  }

  read() {
    //...
  }
}

We have the Student subclass that has its own parameters in the constructor.

We use the super keyword to call the parent constructor.

Also, we have a read method that’s exclusive to the Student class.

Type Inference for Subclasses

The TypeScript compiler may not be able to infer types from classes.

It’s easy to produce unexpected results by assuming that it has insights into the structure of the created objects.

For instance, if we have:

class Person {
  constructor(public name: string) {}
}

class Student extends Person {
  constructor(readonly id: number, public name: string) {
    super(name);
  }

  read() {
    //...
  }
}

const person = new Person("james");
const student = new Student(1, "james");

const people: (Person | Student)[] = [person, student];
people.forEach(p => {
  if (p instanceof Person) {
    console.log("Person");
  } else {
    console.log("Student");
  }
});

We’ll see that both objects are considered to be an instance of Person.

So we get 'Person' logged for both objects.

Abstract Class

Abstract classes are classes that can’t be instantiated directly and are used to describe common functionality that must be implemented by subclasses.

It forces subclasses to adhere to a specific structure but allows for class-specific implementations of specific methods.

To define one, we can use the abstract keyword.

For instance, we can write:

abstract class Person {
  constructor(public name: string) {}
  getName(): string {
    return this.name;
  }
  abstract getSpecificDetails(): string;
}

class Student extends Person {
  constructor(public id: string, public name: string) {
    super(name);
  }

  getSpecificDetails() {
    return `${this.id} - ${this.name}`;
  }
}

The code above has an abstract Person class with a getName method and a constructor.

Then we have the Student class that implements the Person class.

This is indicated with the extends keyword.

We implemented the getSpecificDetails and return a string.

Type Guarding an Abstract Class

Abstract classes are implemented as regular classes in JavaScript generated by the TypeScript compiler.

The TypeScript compiler prevents us from creating objects with abstract classes.

However, this isn’t carried over to the JavaScript code, which means that we may be able to create objects with classes that are declared abstract in our TypeScript code.

Using Interfaces

Interfaces are similar to object shape types.

A class can implement an interface, which means it must conform to the interface.

For instance, we can write:

interface Person {
  name: string;
  getName(): string;
}

class Employee implements Person {
  constructor(public name: string) {}
  getName() {
    return this.name;
  }
}

We have a Person interface that has the name and getName members.

Then since Employee implements the Person interface as indicated by the implements keyword, we’ve to include both members.

The interface keyword indicates that it’s an interface.

Merging Interface Declarations

We can merge multiple interfaces with the | to form a union or & to form an intersection.

It’s like unions or intersections with object shape types. Intersections mean all members of both interfaces must be included in our object.

Union means that any member of the interface can be included.

Implementing Multiple Interfaces

We can implement multiple interfaces in one class. Then we need to implement all the members of both interfaces.

For instance, we can write:

interface Person {
  name: string;
  getName(): string;
}

interface Owner {
  ownedItems(): string[];
}

class Employee implements Person, Owner {
  constructor(public name: string) {}
  getName() {
    return this.name;
  }

  ownedItems() {
    return ["food"];
  }
}

We have an Employee class that implements both Person and Owner interfaces.

It has all the members from both.

Conclusion

We can implement inheritance with TypeScript. It improves the class syntax’s inheritance syntax.

TypeScript has abstract classes and interfaces to enforce the implementation of classes.

Categories
TypeScript

Using TypeScript — Merging Methods and Classes

TypeScript is a natural extension of JavaScript that’s used in many projects in place of JavaScript.

However, not everyone knows how it actually works.

In this article, we’ll look at how to work with objects and classes in TypeScript.

Merging Methods

If we have 2 object types that have the same method name, then TypeScript will try to create a function with the signature being an intersection of both.

For instance, if we have:

type Cat = {
  name: string;
  speak: (words: number) => string;
};

type Animal = {
  name: string;
  speak: (words: string) => string;
};

const person: Cat | Animal = {
  name: "james",
  speak(words) {
    return words;
  }
};

Then we can’t pass in anything into words since words would have the type number & string which is an impossible intersection.

Therefore, it’s impossible to pass anything into the speak method.

Constructor Functions

Objects can be created with constructor functions in JavaScript.

They can also be used with TypeScript code, but the way they are supported isn’t intuitive and not as elegant as the way classes are handled.

For instance, we can write:

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

In JavaScript to create a constructor function.

In TypeScript, we would get an error for this for having the implicit any type if we have noImplicitAny .

Therefore, we need a way to specify the type of this if we have that set to true .

For instance, we can write:

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

The this parameter must be the first parameter.

It won’t be in the built JavaScript code. The name parameter will still be treated as the first parameter in the compiled JavaScript code.

Classes

TypeScript doesn’t have good support for constructor functions.

Its focus is on improving the class syntax.

The class syntax is good for people that are familiar with other programming languages to get acquainted with Typescript.

We’ve to declare instance properties and their types in TypeScrtipt classes.

This makes classes more verbose, but it allows the constructor parameter types to be different from the type of the instance properties to which they’re assigned.

Objects are created from classes using the new keyword.

The compiler understands the use of the instanceof keyword for narrowing classes.

For instance, we can write:

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

We have the instance variable declaration on top and we assign the value for it in the constructor.

Access Control Keywords

TypeScript provides access control keywords to control access to the object’s instance properties.

This is something that isn’t available in JavaScript.

There are 3 access control keywords.

public allows free access to a property or method and it’s the default keyword.

private restricts access to the class that defines the property or method it’s applied to.

protected restricts access to the class that defines the property or method it’s applied to and its subclasses.

We add the access control keyword before the instance variable name:

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

The built JavaScript code won’t have these keywords, so we can’t rely on them to shield data from the outside.

Ensuring Instance Properties are Initialized

The strictPropertyInitialization compiler option can be set to true to ensure that all instance properties are initialized.

The strictNullCheck option should be enabled for this to work.

Read-Only Properties

The readonly keyword can be used to create instance properties where the value is assigned in the constructor only.

It can’t be assigned a value anywhere else.

For instance, we can write:

class Person {
  public name: string;
  readonly id: number;
  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }
}

We added the readonly keyword as we did with the access modifiers.

Simplifying Class Constructors

We can simplify class constructors by moving the instance variables into the constructor signature.

For instance, we can write:

class Person {
  constructor(readonly id: number, public name: string) {}
}

That’s exactly the same as what we have in the previous example.

It’s just shorter.

So if we instantiate it as follows:

const person = new Person(1, "james");

We get the same result.

Conclusion

TypeScript will merge methods from class when we create an intersection type.

Also, TypeScript adds enhancements to the JavaScript class syntax to make everything clearer.

Categories
TypeScript

Using TypeScript — Functions

TypeScript is a natural extension of JavaScript that’s used in many projects in place of JavaScript.

However, not everyone knows how it actually works.

In this article, we’ll look at how to use functions in our TypeScript code.

Defining Functions

TypeScript enhances the JavaScript syntax for functions by making them more predictable.

We can add the assumptions explicit so that it can be checked by the TypeScript compiler.

For instance, the following is a regular JavaScript function:

const getTax = price => {
  return price * 0.2;
};

It has no data type annotations and we can pass in anything as an argument.

Redefining Functions

TypeScript provides warnings for implicitly inferred types and explicit data type annotations.

As long as the TypeScript compiler can check the data types, we’ll get errors for any data type mismatch.

If we want a function to be able to process different types of data, we define one function to do everything.

JavaScript doesn’t allow function overloading, so we can only define one function with a given name.

Function Parameters

Function parameters are processed differently with TypeScript than JavaScript.

TypeScript expects the same number of arguments as there are parameters.

If the number of arguments doesn’t match the number of parameters, then we’ll get errors from the compiler.

The compiler has the noUnusedParameters option to warn us if a function defines a parameter that it doesn’t use.

Optional Parameters

Function parameters are mandatory by default in TypeScript.

However, we can make them optional with the ? symbol.

For instance, we can write:

const getTax = (price: number, discount?: number) => {
  return price * 0.2 - discount;
};

Then discount would be optional. price is mandatory since there’s no ? after the name.

To make sure that we don’t have unexpected results when we do calculations, we should make sure that discount is number.

We can use the || to do that:

const getTax = (price: number, discount?: number) => {
  return (price * 0.2) - (discount || 0);
};

Then we don’t have to worry about discount being undefined.

Function Parameter with a Default Value

We can have a parameter with a default value. The syntax is inherited from JavaScript.

For instance, we can write:

const getTax = (price: number, discount: number = 0) => {
  return price * 0.2 - discount;
};

We set discount ‘s to 0 as the default value.

Now we don’t have to check for undefined value when we call getTax .

Rest Parameter

The rest parameter is an alternative to default parameters. It allows a function to accept a variable number of arguments. They’re grouped and presented together as an array.

A function can only have one rest parameter. Also, it must be the last parameter.

For instance, we can write:

const getTax = (price: number, ...fees: number[]) => {
  return price * 0.2 - fees.reduce((total, fee) => total + fee, 0);
};

fees is set to the number[] type so that we can only pass in numbers as arguments.

In the function body, we can add all the values safely without worrying that any argument is not a number.

Type Annotations for Function Parameters

Type annotations for function parameters can be added, as we can see from the previous examples, we added data annotations to parameters, including the rest parameter.

Null Parameter Values

null and undefined can be used as values of all types in TypeScript.

Therefore, we can pass in null or undefined as values of functions.

To prevent this from happening, we can use strictNullChecks to disable the use of null and undefined as values of all types.

Function Results

We can add return type data annotations so that we can return something with the type that we want.

For instance, we can write:

const getTax = (price: number, ...fees: number[]): number => {
  return price * 0.2 - fees.reduce((total, fee) => total + fee, 0);
};

Now we only can only return a number.

However, it implicitly can also return undefined but this is made impossible with the type annotation for the parameters.

Conclusion

TypeScript lets us add more data type safety to JavaScript functions by adding data type annotations to parameters.

Also, we can set default values so that we won’t have undefined when we call functions without some arguments.

TypeScript will also check if the number of parameters and arguments are matched by default unless we specify a rest parameter.