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.

Categories
TypeScript

Using TypeScript — Object Types and Intersections

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 in TypeScript by creating intersection types and checking object structures.

Checking Properties

This means that we can’t use the typeof operator to check the type of objects.

Instead, we must find better alternatives.

One way is to check if a property is in the object.

For instance, we can use the in operator:

arr.forEach(a => {
  if ("breed" in a) {
    console.log("animal");
  } else {
    console.log("person");
  }
});

The in operator checks if the 'breed' property is in an object.

It checks both its own and inherited properties.

And it returns true if a property is found in any of those places and false otherwise.

However, this doesn’t help us if a property is in both types.

Type Predicate Function

We can also check if a property is undefined to check if an object is of a certain type.

For instance, we can write:

arr.forEach(a => {
  if (typeof a.breed !== "undefined") {
    console.log("animal");
  } else {
    console.log("person");
  }
});

This is similar to in except that we used typeof to check for a property instead of using the in operator.

Type Intersections

We can use type intersections to define 2 object types that are combined together.

A variable assigned to an object type should have all the properties from both types rather than some of them like union types.

For instance, we can create an intersection type and assign it to a value by writing:

type Thing = { name: string };
type Animal = { breed: string };

const animal: Thing & Animal = {
  name: "james",
  breed: "dog"
};

We defined a Thing type and an Animal and combined them into an intersection type by using the & operator.

This means that animal must have properties from both Thing and Animal included.

If we skip one or more of them, then the TypeScript compiler will give us an error.

For instance, if we have:

const animal: Thing & Animal = {
  name: "jame"
};

Then we’ll get ‘Type ‘{ name: string; }’ is not assignable to type ‘Thing & Animal’. Property ‘breed’ is missing in type ‘{ name: string; }’

We can use intersection types for introducing new properties to existing objects.

For instance, if we have:

type Thing = { name: string };
type Animal = { breed: string };

const thing: Thing = {
  name: "james"
};

const animal: Animal = {
  breed: "cat"
};

const cat: Thing & Animal = {
  ...thing,
  ...animal
};

Then TypeScript compiler won’t throw an error because it recognizes that name and breed are part of the Thing & Animal type.

As long as the property names and the corresponding data type match, the TypeScript compiler can figure out that it matches the structure of an intersection type.

Merging Properties with the Same Type

We can also merge types with overlapping properties.

For instance, if we have:

type Thing = { name: string };
type Animal = { name: string };

Then anything assigned to the Thing & Animal type variable would have the string property called name .

Merging Properties with Different Types

If there are properties with different types, then both properties will be merged together as an intersection.

For instance, if we have:

type Thing = { age: string };
type Animal = { age: number };

const thing: Thing = {
  age: "1"
};

const animal: Animal = {
  age: 1
};

const cat: Thing & Animal = {
  ...thing,
  ...animal
};

Then we would get an error from the compiler since age has type string & number which no value matches.

Therefore, we shouldn’t have properties with different types in the object types that we intersect.

Conclusion

We can create intersection types to merge object types into one type.

Then when a variable or parameter has the intersection type, it must include all the properties of both types, and the data type of the property must be an intersection of data types from both types.

We can check the property type with the in operator or typeof to check the structure of each object to determine their type.