Categories
TypeScript

Using TypeScript — Literal Types, Object Types, and Type Aliases

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 literal value types, object types, and type aliases.

Literal Value Types

A literal value type is a data type with a specific set of values and only the ones specified are allowed.

This lets us treat a set of values as a distinct type.

For instance, we can write:

let val: 1 | 2 = 1;

Then we have val which can take on the value 1 or 2.

If we assigned something that we haven’t explicitly allow, then we get:

let val: 1 | 2 = 100;

Then we get the error ‘Type ‘100’ is not assignable to type ‘1 | 2’.ts(2322)‘

Literal Value Types in Functions

We can use literal types in functions.

This lets us restrict a parameter to allow us to set to only several kinds of values.

For instance, we can write:

const totalPrice = (quantity: 1 | 2, price: number) => {
  return quantity * price;
};

Then we can only pass in 1 or 2 for quantity .

Mixing Value Types in a Literal Value Type

We can narrow value types to a literal type.

For instance, we can write:

const randomNum = () => {
  return Math.floor(Math.random() * 3) as 1 | 2 | 3;
};

Then we narrow the type of the returned value of randomNum from number to 1 | 2 | 3 .

randomNum can only return 1, 2, or 3.

Overloads with Literal Value Types

We can use literal types in function overloads.

For instance, we can write:

function foo(val: 1): "foo";
function foo(val: 2): "bar" | "baz";
function foo(val: number): string {
  switch (val) {
    case 1: {
      return "foo";
    }
    case 2: {
      return "bar";
    }
    default: {
      return "baz";
    }
  }
}

Then we can take on various values because of the overloads.

It can return the value based on the value of val that we pass into foo .

Type Aliases

To avoid repeatedly defining the same type, TypeScript has a type alias feature.

We can assign type combinations to an alias so we can use them throughout our code.

For instance, we can write:

type combo = 1 | 2 | 3;

The type operator lets us define a type alias with the name we want.

In our example, our type alias name is combo .

Objects

JavaScript objects are collections of properties that can be created using the literal syntax, or returned by constructors or classes.

Objects can be altered once they’re created.

We can add or remove properties and receive values of different types.

To provide type features for objects, TypeScript lets us specify the structure of an object.

Object Shape Type Annotations

One way to define the shape of an object is to use the object shape type annotation.

We define the property and the type of each property in the object.

For instance, we can write:

const obj: { foo: number } = { foo: 1 };

The code above defined the object obj which can have the property foo which is a number.

Then we assigned something that has such property and value from the right side.

How Shape Types Fit

An object must define all the properties in the shape with the given data type to match the object shape type annotation.

We’ll get an error if the property structure or the data type doesn’t match.

Optional Properties

We can make a property of an object type optional with a ? .

For instance, we can write:

const obj: { foo: number; bar?: sting } = { foo: 1 };

Then bar is optional and we don’t have to add a value to it.

Conclusion

We can define literal types to restrict the value of a parameter or variable to a few values.

Also, we can define object types with properties and types of them.

Finally, we can define type aliases so that we don’t have to repeat the same data type annotation everywhere.

Categories
TypeScript

Using TypeScript — Extending 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 extend generic types in TypeScript.

Extending Generic Classes

We can add extra features to existing classes.

For instance, we can write:

class Collection<T extends { name: string }> {
  protected 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];
  }
}

class SearchableCollection<T extends { name: string }> extends Collection<T> {
  find(name: string): T | undefined {
    return this.items.find(item => item.name === name);
  }
}

In the example above, we have the { name: string } type which T must match.

At least we need the name string property.

For instance, we can write:

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

const persons: SearchableCollection<Person> = new SearchableCollection<Person>([
  {
    name: "james",
    age: 1
  }
]);

Person has the name string property and the age number property, but we can still use it.

As long as the object has the name property and its value is a string, we can use it.

Restricting the Generic Type Parameter

We can assign a more restrictive type than the one that’s specified in the type parameter.

For instance, we can write:

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

class Owner extends Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    super(name);
    this.age = age;
  }
}

class Collection<T extends Person | Owner> {
  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 use it with a more restrictive type by writing:

const items: Collection<Owner> = new Collection<Owner>([new Owner("james", 1)]);

Owner is more restrictive than Person since it’s a subclass of Person .

Now if we try to add a Person instance into the array we pass into the constructor, we’ll get an error.

For example, if we have:

const items: Collection<Owner> = new Collection<Owner>([
  new Owner("james", 1),
  new Person("jane")
]);

We get the error ‘Property ‘age’ is missing in type ‘Person’ but required in type ‘Owner’.‘

Static Method on a Generic Class

We can define a static method in a generic class.

For instance, we can write:

class Collection<T extends Person | Owner> {
  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);
  }

  static getItem(items: any[], index: number) {
    return items[index];
  }
}

We used the static keyword to indicate that getItem is a static method. We can’t reference any generic type parameters with static methods. The static method is the share across all instances of a class.

We can getItem by writing:

const item: number = Collection.getItem([1, 2, 3], 1);

If we want to add generic type parameters to a static method, then we’ve to add ones that aren’t used by the class.

For example, we can write:

class Collection<T extends Person | Owner> {
  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);
  }

  static getItem<A>(items: A[], index: number) {
    return items[index];
  }
}

We added the A data type placeholder which isn’t used anywhere else.

Then we can call the method by writing:

const item: number = Collection.getItem<number>([1, 2, 3], 1);

Generic Interfaces

We can define generic interfaces.

For instance, we can write:

type Person = {
  name: string;
};

interface Collection<T extends Person> {
  add(...members: T[]): void;
}

Then we can create a class or an object that has the add method that accepts an array with of T type objects that have the members of Person .

Once we have that, we can use the interface as follows:

class DataCollection implements Collection<Person> {
  members: Person[] = [];
  add(...members: Person[]) {
    this.members.push(...members);
  }
}

We set T to Person and then use the Collection interface to enforce our implementation.

Conclusion

We can add generic type parameters to class methods, static methods, and interfaces.

Generic classes can be extended to create subclasses of them, which are also generic.

Likewise, interfaces can also be extended.

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.