Advanced types in Type Script

0
405

Type Script has many advanced type capabilities which help make writing dynamically typed code easy. It also facilitates the adoption of existing JavaScript code, since it lets us keep the dynamic capabilities of JavaScript while using the type-checking capability of Type Script. There are many types of advanced types in Type Script — intersection types, union types, type guards, nullable types, type aliases, and more. In this piece, we will look at the difference between interfaces and type aliases, literal types, and discriminated unions.

Advanced types in Type Script

Difference Between Interfaces and Type Aliases

There are subtle differences between interfaces and type aliases, even though they look similar. Interfaces create a new type that can be used anywhere. However, type aliases are just new names for types that already exist, which includes interfaces. As of TypeScript 2.7, type aliases can be extended by creating new intersection types. This means that something like the following code is valid from TypeScript 2.7 and on:

type Cat = string & { age: number }

For creating new data types, interfaces should be used instead of type aliases.

String Literals

For strings, we can specify what values a string variable can take on by specifying the possible values that it can be assigned, with each value separated by the pipe symbol (|). These can be combined into other types, like union types, type guards and type aliases.

For example, we can write something like the following code:

type PersonName = 'Sathish' | 'Kumar' | 'Ram';
let personName: PersonName = 'Sathish';

If we try to assign n a value that’s not listed in the PersonName type alias, as we do in the following code:

type PersonName = 'Sathish' | 'Kumar' | 'Ram';
let personName: PersonName = 'vijay';

Then we get the following error message from the Type Script compiler:

Type '"vijay"' is not assignable to type 'PersonName'.(2322)

With the pipe notation used for strings and assigned to the type alias, it lets us declare a variable that is like an enum, but not. We can also distinguish overloads by setting the possible string value for strings. For example, we can write:

function echo(name: 'Sathish'): string;
function echo(name: 'Kumar'): string;
function echo(name: 'Ram'): string;
function echo(name: string) {
  return name;
}
echo('Sathish');

If we try to pass in an invalid value, which is any value that’s not listed in the overloads, then we get an error. For example, if we write this:

function echo(name: 'Sathish'): string;
function echo(name: 'Kumar'): string;
function echo(name: 'Ram'): string;
function echo(name: string) {
  return name;
}
echo('Raja');

Then we get these error messages from the Type Script compiler:

No overload matches this call.
Overload 1 of 3, '(name: "Joe"): string', gave the following error.
Argument of type '"foo"' is not assignable to parameter of type '"Joe"'.
Overload 2 of 3, '(name: "Jane"): string', gave the following error.
Argument of type '"foo"' is not assignable to parameter of type '"Jane"'.
Overload 3 of 3, '(name: "Amy"): string', gave the following error.
Argument of type '"foo"' is not assignable to parameter of type '"Amy"'.(2769)

Numeric Literal Types

We can create a literal type with numbers, just as we do with strings. The notation to do this is the same, except that we replace the valid values for the type with numbers instead. For example, we can write the following code to define a numeric literal type and assign a value to a variable with such a type:

type Num = 1 | 2 | 3;
let num: Num = 1;

What if we try to assign a value that’s not listed in the type alias definition as in the following code?:

type Num = 1 | 2 | 3;
let num: Num = 6;

We get the following error message:

Type '6' is not assignable to type 'Num'.(2322)

Discriminated Unions

We can add a member with a constant value to distinguish between different types that form the union type. This is called a discriminated union. Other names for this include tagged union or algebraic data types. In TypeScript, we can create a discriminated union with three parts. First, we need a common, single instance type of property, which is called the discriminant. Second, we need a type alias that takes the union of those types, which is the union type that we usually create. Finally, we need a type guard on the property.

We can create a discriminated union with Type Script with something like the following code:

interface Cat {
  kind: "cat";
  whiskerLength: number;    
}
interface Dog {
  kind: "dog";
  snoutSize: number;    
}
interface Bird {
  kind: "bird";
  beakSize: number;
}
type Animal = Cat | Dog | Bird;
function getSize(animal: Animal) {
  switch (animal.kind) {
    case "cat": return animal.whiskerLength;
    case "dog": return animal.snoutSize;
    case "bird": return animal.beakSize;
  }
}

In the example above, we have the common kind member that’s available in all the interfaces. Then we creat the union Animal type, which is a union of the Cat , Dog and Bird types. This means that the kind property of an Animal object can take on the values cat , dog , or bird . Then, in the getSize function, we check the value of the kind property. In the switch statement, we check the kind property, we check the value of the property, and TypeScript figures which type the animal parameter is from inside the case block, which is our type guard.

We should add type guards to check for all the types that form the union type. What if we add another type to the Animal union type that we have above but didn’t update the getSize function, as in the code below?:

interface Cat {
  kind: "cat";
  whiskerLength: number;    
}
interface Dog {
  kind: "dog";
  snoutSize: number;    
}
interface Bird {
  kind: "bird";
  beakSize: number;
}
interface Hippo {
  kind: "hippo";
  teethSize: number;
}
type Animal = Cat | Dog | Bird | Hippo;
function getSize(animal: Animal) {
  switch (animal.kind) {
    case "cat": return animal.whiskerLength;
    case "dog": return animal.snoutSize;
    case "bird": return animal.beakSize;
  }
}

We would get the following error messages from the TypeScript compiler:

Not all code paths return a value.(7030)

Instead, we should write to cover all cases:

interface Cat {
  kind: "cat";
  whiskerLength: number;    
}
interface Dog {
  kind: "dog";
  snoutSize: number;    
}
interface Bird {
  kind: "bird";
  beakSize: number;
}
interface Hippo {
  kind: "hippo";
  teethSize: number;
}
type Animal = Cat | Dog | Bird | Hippo;
function getSize(animal: Animal) {
  switch (animal.kind) {
    case "cat": return animal.whiskerLength;
    case "dog": return animal.snoutSize;
    case "bird": return animal.beakSize;
    case "hippo": return animal.teethSize;
  }
}

There are subtle differences between interfaces and type aliases, even though they look kind of the same. Interfaces create a new type that can be used anywhere. However, type alias is just a new name for types that already existed, which includes interfaces. In Type Script, we can create string or number literals to restrict the possible values that a variable of these types can take on. We can also create discriminated unions, by adding a singleton property to each interface and set a unique value to each. Then we can check for that property’s value with type guards in our code to get the property that we want.