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.


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

查看所有标签

猜你喜欢:

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

互联网心理学

互联网心理学

雷雳 / 北京师范大学出版社 / 2016-6-1 / CNY 99.00

☆人为什么要使用互联网? ☆为什么越来越多的人更喜欢在网上畅所欲言? ☆网络行为背后的心理机制又是什么? ☆虚拟网络世界又是如何改变了我们? 当连接万物的互联网遇见无处不在的心理学,当虚拟空间生长出真实的“心理特性”,我们需要用心理学的方式,重新思考互联网背后的人与社会。这是一部汇集前沿学者智慧、充满探索精神的佳作,该书从心理学视角切入,透过文化多样性和环境多样性,详细解读......一起来看看 《互联网心理学》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具