Categories
TypeScript

Using TypeScript — Tuples and Enums

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 tuples and enums in our TypeScript code.

Tuples

Tuples are fixed lengths arrays, and each element can have a different type.

It’s a TypeScript-specific data structure that’s transformed into regular JavaScript arrays.

For instance, we can define one by writing:

let person: [string, number] = ["james", 100];

Tuples are defined using square brackets with the types of each element inside separated by commas.

We defined a tuple of 2 elements in the example above, where the first element must be a string and the 2nd must be a number.

Processing Tuples

TypeScript enforces the actions that we can take with arrays.

We can use them with standard JavaScript features since they’re just implemented with regular arrays.

For instance, we can write:

const strings: string[] = person.map(e => e.toString());

We called the map on it like a regular array.

Other array methods and operations are also available to tuples.

Using Tuple Types

Tuples have a distinct type that can be used just like any type.

This means that we can create arrays of tuples, union types with tuples, and type guards to restrict the values that can be in tuple types.

For instance, we can write:

let person: [string, number] = ["james", 100];
person.forEach(e => {
  if (typeof e === "number") {
    console.log(`number: ${e}`);
  } else if (typeof e === "string") {
    console.log(`string: ${e}`);
  }
});

We called the forEach method to loop through the entries and display the type with the data of each entry of the tuple.

The typeof operator lets us check the type and do something according to each type.

Enums

Enum let’s has created a collection that is used by name.

It makes code easier to read and ensures that fixed sets of values are used consistently.

We can define a TypeScript enum as follows:

enum Fruit {
  APPLE,
  ORANGE,
  GRAPE
}

We used the enum keyword and a bunch of constants inside to define an enum.

Then we can use it by writing:

const apple = Fruit.APPLE;

By default an enum value would be mapped to a number, so apple would be 0 since Fruit.APPLE is 0.

Likewise Fruit.ORANGE is 1 and Fruit.GRAPE is 2.

Since enum values are JavaScript number values by default, we can assign to a number variable:

const apple: number = Fruit.APPLE;

and the TypeScript compiler won’t give us any errors.

We can’t compare values from different enums.

For instance, we can’t write:

enum Fruit {
  APPLE,
  ORANGE,
  GRAPE
}

enum Gender {
  MALE,
  FEMALE
}

const apple: number = Fruit[Gender.FEMALE];

We can also assign our own values to an enum, so we can write:

enum Fruit {
  APPLE,
  ORANGE = 10,
  GRAPE
}

Then Fruit.ORANGE is 10 and Fruit.GRAPE is 11.

It’ll start incrementing from the set value if the constant is after the that has a value assigned to it.

String Enums

TypeScript enums have number values by default.

However, we can assign a string value to it.

For instance, we can write:

enum Fruit {
  APPLE = "apple",
  ORANGE = "orange",
  GRAPE = "grape"
}

Once we set an enum constant to a string, we’ve to set them all to a string.

Otherwise, we’ll get an error.

Limitations of Enums

There are some limitations with enums.

This is because the enum feature is implemented with the TypeScript compiler.

For instance, given that we have:

enum Fruit {
  APPLE,
  ORANGE,
  GRAPE
}

We can assign a value to it by writing:

const fruit: Fruit = 100;

The TypeScript compiler doesn’t prevent us from assigning invalid values to a variable with an enum type.

However, this isn’t a problem with string enums.

Also, the typeof operator can’t distinguish between enum and number values.

For instance, if we write:

enum Fruit {
  APPLE,
  ORANGE,
  GRAPE
}

let fruit: Fruit = Fruit.APPLE;
if (typeof fruit === "number") {
  console.log("fruit is a number");
}

Then we get 'fruit is a number' logged.

Conclusion

We can use tuples as fixed-length arrays and they can have different types of data in them.

Tuples can call array methods since they are JavaScript arrays with some restrictions.

Enums lets us define constant values under one umbrella.

They can have string or number values.

Categories
TypeScript

Using TypeScript — Generic Types

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 generic types in TypeScript.

Generic Types

Generic types allow us to write code that can have different behaviors when we plug in different types to it.

Creating Generic Classes

A generic class is a class that has a generic type parameter. A generic type parameter is a placeholder for a type that’s specified when the class is used to create a new object.

For instance, we can write:

class Collection<T> {
  private items: T[] = [];
  constructor(items: T[]) {
    this.items.push(...items);
  }

  add(items: T) {
    this.items.push(items);
  }

  remove(index: number) {
    this.items.splice(index, 1);
  }

  getItem(index: number): T {
    return this.items[index];
  }
}

T is the placeholder for a data type.

We can instantiate this class by writing:

const numbers: Collection<number> = new Collection<number>([1, 2, 3]);

We put in the number type in place of T . Then we can add numbers into the items array of our Collection instance.

A generic class can have more than one data type parameter.

Generic Type Arguments

number is the data type argument and Collection is the generic class in the example above.

Different Type Arguments

We can have different data type arguments inserted as a type argument.

For instance, in addition to number , we can put in string instead:

const strings: Collection<string> = new Collection<string>(["foo", "bar"]);

Generic Type Values

We can restrict the type of value in our generic type code by using the extends keyword.

For instance, we can write:

class Collection<T extends number | string> {
  private items: T[] = [];
  constructor(items: T[]) {
    this.items.push(...items);
  }

  add(items: T) {
    this.items.push(items);
  }

  remove(index: number) {
    this.items.splice(index, 1);
  }

  getItem(index: number): T {
    return this.items[index];
  }
}

Then we can insert the type parameter, which are number , string , or anything narrower.

For instance, we can write:

const numbers: Collection<number> = new Collection<number>([1, 2, 3]);

or:

const strings: Collection<1> = new Collection<1>([1, 1]);

They both work since number and 1 are both subsets of numbers.

extends means that we can assign the subset of one of those types listed.

Constraining Generic Types Using Shape Types

We can also use shape types to restrict generic types.

For instance, we can write:

class Collection<T extends { name: string }> {
  private items: T[] = [];
  constructor(items: T[]) {
    this.items.push(...items);
  }

  add(items: T) {
    this.items.push(items);
  }

  remove(index: number) {
    this.items.splice(index, 1);
  }

  getItem(index: number): T {
    return this.items[index];
  }
}

interface Person {
  name: string;
}

const people: Collection<Person> = new Collection<Person>([{ name: "james" }]);

We have:

T extends { name: string }

to restrict our type inside our Collection to be only objects with the name key.

Any other type with the same shape would work.

Multiple Type Parameters

A class can have multiple type parameters.

We can add a second type parameter to our Collection class:

class Collection<T, U> {
  private items: (T | U)[] = [];
  constructor(items: T[], moreItems: U[]) {
    this.items.push(...items, ...moreItems);
  }

  add(items: T) {
    this.items.push(items);
  }

  remove(index: number) {
    this.items.splice(index, 1);
  }

  getItem(index: number): T | U {
    return this.items[index];
  }
}

We have the U parameter to add let us add objects of a different type into this.items .

Then we can write:

const items: Collection<number, string> = new Collection<number, string>(
  [1, 2, 3],
  ["foo", "bar"]
);

to create a Collection instance with items that can have numbers or strings.

Additional type parameters are separated with commas like regular functions or method parameters.

Applying Type Parameter to a Method

We can also apply type parameters to a method.

For instance, we can write:

class Collection<T, U> {
  private items: (T | U)[] = [];
  constructor(items: T[], moreItems: U[]) {
    this.items.push(...items, ...moreItems);
  }

  add(items: T) {
    this.items.push(items);
  }

  remove(index: number) {
    this.items.splice(index, 1);
  }

  getItem(index: number): T | U {
    return this.items[index];
  }

  searchItemsByType<U>(searchData: U[], target: U): U[] {
    return searchData.filter(s => s === target);
  }
}

We have getItemsByType which has the searchData parameter with type U[] and target with type U .

Then we can use it by writing:

const results = items.searchItemsByType<string>(["baz", "foo"], "foo");

Then we search the collection of strings for the 'foo' string with getItemsBuType.

Conclusion

We can add generic type parameters to classes to make them work with different types of data.

It can be one or more than one.

Categories
TypeScript

Using TypeScript — Interfaces and Abstract 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 interfaces and abstract classes in TypeScript.

Extending Interfaces

Like classes, interfaces can be extended. We use the same approach as extending classes.

For instance, we can write:

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

interface Owner extends Person {
  item: string;
  getItemDetails(): string;
}

We use the extends keyword like we do with classes.

It has the same meaning. The members from the parent interfaces are inherited by the child interface.

For instance, we can write:

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

interface Owner extends Person {
  item: string;
  getItemDetails(): string;
}

class ThingOwner implements Owner {
  constructor(public name: string, public item: string) {}

  getName() {
    return this.name;
  }

  getItemDetails() {
    return this.item;
  }
}

We have all the members from each interface implemented.

The implements keyword means that we must implement everything in the child interface including inherited members.

Interfaces and Shape Types

Interfaces and shape types are different, even though they might look similar.

Interfaces can be used with the implements keyword to make a class implement everything in the interface.

Shape types can only be assigned directly to a variable. However, interfaces can conform to shape types.

For instance, we can use the extends keyword with a shape type:

type Person = {
  name: string;
  getName(): string;
};

interface Owner extends Person {
  item: string;
  getItemDetails(): string;
}

class ThingOwner implements Owner {
  constructor(public name: string, public item: string) {}

  getName() {
    return this.name;
  }

  getItemDetails() {
    return this.item;
  }
}

The extends keyword indicates that our interface includes all the members of the shape type.

Optional Interface Properties and Methods

Interface properties and methods can be optional. The ? indicates that it’s an optional member.

For instance, we can write:

type Person = {
  name: string;
  getName?(): string;
};

interface Owner extends Person {
  item: string;
  getItemDetails?(): string;
}

class ThingOwner implements Owner {
  constructor(public name: string, public item: string) {}

  getName() {
    return this.name;
  }

  getItemDetails() {
    return this.item;
  }
}

We made getName and getItemDetails optional with the ? symbol.

Optional interface features can be defined through interface types without causing compiler errors. But we must be sure that we don’t receive undefined values since they may not exist in the returned object.

Abstract Interface Implementation

We can have abstract interfaces in our code.

We can use the abstract keyword with the members of the abstract class so that the implementation is in the concrete classes rather than the abstract class itself.

For instance, we can write:

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

abstract class Owner implements Person {
  name: string;
  abstract getName(): string;
}

class ThingOwner implements Owner {
  constructor(public name: string, public item: string) {}

  getName() {
    return this.name;
  }
}

We have an Owner abstract class that implements the Person interface.

The ThingOwner class implements the abstract class with a concrete implementation of getName .

Dynamically Creating Properties

JavaScript allows new properties to created on an object by assigning value to an unused property name.

With TypeScript, we can allow dynamic properties on our interfaces by providing index signatures.

For instance, we can write:

interface Person {
  name: string;
  [prop: string]: any;
}

to add an index signature to the Person interface.

The line:

[prop: string]: any;

is the index signature, and it allows us to add any string property to anything that has Person as a type.

We can assign any value to the dynamic property.

Conclusion

We can extend interfaces as we do with subclasses.

Also, we can implement shape types with interfaces.

Interfaces can enforce the structure of classes and objects.

Also, abstract class can enforce the structure of objects.

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.