Friday, May 28, 2021

3 Ways to Name Types in Typescript

TypeScript type's system is structural and hence, it allows for the easy creation of types based on the shape of data. These types can be created and used without having a name. For example:

let john: {name: string, age: number} = { 
  name: "John Doe", 
  age: 30
}

Where {name: string, age: number} is the type defined.

But there is a problem with using the type definition this way. If we want to create another person and annotate another variable with the type, we would need to repeat ourselves:

let john: {name: string, age: number} = {
  name: "John Doe", 
  age: 30
}

let jane: {name: string, age: number} = {
   name: "Jane Doe", 
   age: 45
}

This is not dry.

It is not dry because we have to repeat the types. A more efficient approach would be to define the type once, give it a name and then use the name when applying the type annotation. Fortunately, we can do exactly this.

TypeScript provides mechanisms for giving a name to a type, which can then be used when applying type annotations. We are going to look into the 3 different mechanisms through which this can be done. Namely: via classes, type aliases and interfaces.

Classes

A class can be seen as a blueprint for the creation of objects. For example given the following class definition:

class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }

}

We can use it to create various instance objects:

// class definition
class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }

}

// creating instances
let john = new Person("John Doe", 30)
let jane = new Person("Jane Doe", 45)

The interesting thing with classes is they are also a mechanism for defining and naming types. They not only serve as a blueprint for object creation, but they also create a type, which is named.

The Person class we defined also simultaneously creates a Person type, which we can use as a type annotation:

// class definition
class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }

}

// annotating instances
let john: Person = new Person("John Doe", 30)
let jane: Person = new Person("Jane Doe", 45)

Type Aliase

Type aliases are another mechanism for naming types in TypeScript. It is a feature that makes it possible to give a name (or alias) to another type.

type Person = {name: string, age: number}
type MyString = string
// An alias can be given for an existing alias
type OhMyString = MyString

Hence it is best to see type aliases as a way to give a name to an already existing anonymous type, primitive type, or even another type alias.

Interfaces

Interfaces are another mechanism for creating types and naming them in TypeScript. It takes the following form:

interface Person {
    name: string,
    age: number
}

As can be seen, even though interface looks similar to using type aliases, they are not the same. Unlike type aliases, interfaces actually create a new type, and gives it a name, while type aliases only create a name for an existing type.

The 3 mechanisms shown thus far (classes, type aliases, and interfaces) provide us with 3 ways for doing the same thing: the naming of types. Even though they allow us to achieve similar things, there are some differences. In the next section, we take a look at some of these differences.

Difference Between Classes, Type Aliases, and Interfaces


The Presence or absence of a constructor

Classes differ from type aliases and interfaces in that it also provides a constructor that can be used to create well form instances of values for the type being defined. This is achieved by using the familiar new keyword:

// class definition
class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }

}

// creating instances using constructor
let john: Person = new Person("John Doe", 30)

Constructors are not created as part of using type aliases nor interfaces.

Extension mechanism

Classes, type aliases, and interfaces all provide a mechanism for type extension, which is the ability to create a new type based on an already existing type. The only difference is the syntax used.

Extending types via classes takes the following form:

//type definition 
class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
      
}

// type extension
class Employee extends Person {
       isManager: boolean;
       
       constructor(name: string, age: number, isManager: boolean) {
           super(name, age)
           this.isManager = isManager
       }

}

doing the same via type aliases look like the following:

// type definition
type Person = {
    name: string
    age: number
}

// type extension
type Employee = Person & {isManager: boolean}

and finally with interface we have:

interface Person {
    name: string
    age: number
}

interface Employee extends Person {
    isManager: boolean
}

The syntax for extension with classes and interfaces looks similar while it is different for type aliases. Extending type aliases makes use of intersection types, which is a topic that is explored in details in Union and Intersection Types in TypeScript

Possibility for multiple definition

Interfaces differ from the other mechanisms in that, the type being named can be spread across multiple definitions.

//first definition 
interface Employee {
    name: string
}

// second definition
interface Employee {
    age: number
}

// third definition
interface Employee {
    isManager: boolean
}

// Merged Employee type being used
let john: Employee = {
    name: "John Doe",
    age: 30,
    isManager: true
}

TypeScript will take all the definitions of the interface Employee, and merges them into one. This is not possible with type aliases nor interfaces as their definition needs to be unique.



I am writing a book: TypeScript Beyond The Basics. Sign up here to be notified when it is ready.

No comments: