Structural type system and polymorphism in TypeScript. Type guards with predicates

栏目: IT技术 · 发布时间: 4年前

内容简介:TypeScript is a superset of JavaScript. Any JavaScript code is a valid TypeScript code, as long we set the compiler not to be strict. Therefore, TypeScript aims to be as flexible as possible so that it can apply to various situations. In this article, we l

TypeScript is a superset of JavaScript. Any JavaScript code is a valid TypeScript code, as long we set the compiler not to be strict. Therefore, TypeScript aims to be as flexible as possible so that it can apply to various situations. In this article, we look into type compatibility in TypeScript and explain what a structural type system is.

Type Compatibility in TypeScript

There are a lot of languages with a nominal type system . The above means that the two variables are compatible if they are of the same type. Let’s examine this C# code :

public class Employee
{
    public string name;
    public Employee(string name)
    {
        this.name = name;
    }
}
public class Person
{
    public string name;
    public Person(string name)
    {
        this.name = name;
    }
}
Employee john = new Employee('John');
john = new Person('John');

The above C# code causes an error. Due to a nominal type system, Employee and  Person are not compatible. Similarly, such situations occur in languages like Java and C++.

The above behavior might help us to prevent mismatching types, but it is not very flexible. To give us more freedom, TypeScript implements a structural type system .

Structural type system

In a language with a structural type system, two types are compatible judging by their structure , instead of the name. The above allows TypeScript to adjust to the way that we often write the JavaScript code.

type Employee = {
  name: string;
}
 
class Person {
  public constructor (readonly name: string) {}
}
 
const john: Employee = new Person('John');

Above, we simplify the assignment of properties in the constructor of the Person class with the use of the readonly keyword

In TypeScript, the above code is perfectly valid. Going even further, we can use types that are not identical when it comes to its structure.

Structural subtyping

For one type to be compatible with the other, it needs to have at least the same properties .

interface Employee {
  name: string;
  workplaceType: string;
}
 
interface Person {
  name: string;
}
 
function printName(person: Person) {
  console.log(person.name);
}

Above, we have the printName function. Since the  Employee has all the properties of a  Person .

const john: Employee = {
  name: 'John',
  workplaceType: 'Music store'
}
 
printName(john);

The fact that the Person is compatible with the Employee does not mean that it works the other way around.

function printWorkplace(person: Employee) {
  console.log(person.name);
}
const john: Person = {
  name: 'John',
}
 
printWorkplace(john);
Argument of type ‘Person’ is not assignable to parameter of type ‘Employee’.  Property ‘workplaceType’ is missing in type ‘Person’ but required in type ‘Employee’.

The above happens because the Person does not have all the properties of the  Employee .

Similar subtyping happens when the Employee extends the  Person .

interface Person {
  name: string;
}
 
interface Employee extends Person {
  workplaceType: string;
}

In languages with nominal subtyping , it would be the only way to achieve compatible subtypes. Thanks to TypeScript being a structurally typed language, instead, “accidental” subtypes work issue-free.

Polymorphism

Calling the printName ( person : Person ) function using the  Employee is an example of  polymorphism . Since we know that the employee has all the properties of a person, we can treat its instance as such.

The most straightforward way to visualize it is with the use of an example with shapes and calculating their areas.

interface Shape {
  getArea(): number;
}
 
class Circle {
  constructor(readonly radius: number) {}
  getArea() {
    return Math.pow(this.radius, 2) * Math.PI;
  }
}
 
class Square {
  constructor(readonly size: number) {}
  getArea() {
    return Math.pow(this.size, 2);
  }
}

Although neither Circle nor Square extends  Shape explicitly, all of them have the  getArea function. If that’s all we need, we can treat Circle and Square as a Shape .

const shapes = [
  new Circle(10),
  new Square(5),
  new Circle(2),
  new Square(25)
]
 
const sortedShapes = shapes.sort((firstShape: Shape, secondShape: Shape) => (
  firstShape.getArea() - secondShape.getArea()
))

Differentiating types with Type Guards

We don’t always have such straightforward types. Sometimes, we need to deal with unions and the unknown . Thankfully, TypeScript has mechanisms to help us with that. Such a situation might occur when we fetch the data from various APIs. Let’s say we want to fetch a user and print his workplace type if he is an employee.

function printWorkplaceType(employee: Employee) {
  console.log(employee.workplaceType);
}
type FetchUser = () => Promise<unknown>;
fetchUser()
  .then((user) => {
    printWorkplaceType(user);
  })

Unfortunately, the above does not work. We experience an error:

Argument of type ‘unknown’ is not assignable to parameter of type ‘Employee’.  Type ‘{}’ is missing the following properties from type ‘Employee’: name, workplaceType

This is because we are not sure if what we fetch is a proper employee. At first glance, we might want to check the existence of the workplaceType property.

fetchUser()
  .then(user => {
    if (user.workplaceType) {
      printWorkplaceType(user);
    }
  })

The above does not work either, because Property 'workplaceType' does not exist on type 'unknown' . Even if we could check this property, the compiler wouldn’t treat it as a proper Employee .

We also can’t use the instanceof operator, because the Employee is just an interface. A solution to this issue are Type Guards .

Defining Type Guards

A type guard is a function that guarantees a type during a runtime check .

function isEmployee(user: any): user is Employee {
  return Boolean(user.workplaceType && user.name);
}

The user is Employee is a  type predicate . Type predicates are a special return type that signals the type of a particular value.

Now, we can easily implement it in our logic to make sure that what we fetch is a proper Employee .

fetchUser()
  .then(user => {
    if (isEmployee(user)) {
      printWorkplaceType(user);
    }
  })

In the above code, we check the type in the runtime so that we can safely call the printWorkplaceType function.

We can make our code a bit cleaner using the in operator.

The  in  operator returns  true if the specified property is in the specified object or its prototype chain.

function isEmployee(user: any): user is Employee {
  return 'workplaceType' in user && 'name' in user;
}

Summary

In this article, we’ve reviewed two types of systems: structural and  nominal . We’ve also looked through the consequences and reasons of TypeScript having the structural type system. We explained what polymorphism is and how we can apply it to TypeScript without extending interfaces explicitly. To help us in some situations, we’ve used type guards with type predicates and the in operator.


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

MongoDB

MongoDB

Kristina Chodorow / O'Reilly Media / 2013-5-23 / USD 39.99

How does MongoDB help you manage a huMONGOus amount of data collected through your web application? With this authoritative introduction, you'll learn the many advantages of using document-oriented da......一起来看看 《MongoDB》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

html转js在线工具
html转js在线工具

html转js在线工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具