你有没有被 TypeScript 中的“交叉类型”和“继承”搞得头晕目眩?看起来,它们好像在做同一件事?

在 TypeScript 中,交叉类型和继承是两种非常强大的工具,然而,很多开发者在刚接触时常常会混淆它们。两者都能让你在类型系统中“合并”多个对象或类型,但它们在实际用途和语法上却有很大的不同。
你知道什么时候该用交叉类型,什么时候又该用继承吗?来来来~看看你是否正确的理解了交叉类型与继承!

一、继承的基本概念

继承是面向对象编程(OOP)的基石之一,它让一个类可以继承另一个类的属性和方法。在 TypeScript 中,继承通过 extends 关键字实现。
比如说,我们有一个基类叫做“动物”,这个类有一些基本的属性,像“名字”和“年龄”,还有一些通用的方法,比如“吃东西”。

class Animal {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  eat() {
    console.log(`${this.name} is eating.`);
  }
}

接着,还可以有派生类,比如“猫”类和“狗”类,它们继承自“动物”类。这些派生类不仅拥有基类的属性和方法,还可以有自己独特的属性和行为。就像猫可能有“抓老鼠”的方法,狗可能有“看家”·的方法。

class Cat extends Animal {
  catchMouse() {
    console.log(`${this.name} is catching a mouse.`);
  }
}

class Dog extends Animal {
  guardHouse() {
    console.log(`${this.name} is guarding the house.`);
  }
}

在这个例子中,Cat、Dog 继承了 Animal 类,它拥有 Animal 类的属性和方法,并且还可以对方法进行覆盖。
所以我们可以知道继承的用途是:主要用于代码复用和实现多态性,允许开发者创建基于现有类的新的类。

二、交叉类型的基本概念

交叉类型是 TypeScript 中一种非常有趣的类型组合机制。通过交叉类型,你可以将多个类型合并为一个类型,所有的属性和方法都会“合体”成一个新的类型。交叉类型的语法使用 & 操作符。
比如说,我们有一个类型叫 “HasName”,它只有一个 “name” 属性,还有一个类型叫 “HasAge”,它只有一个 “age” 属性。我们可以用交叉类型把它们组合成一个新的类型 “Person”,这个 “Person” 类型就既有 “name” 属性又有 “age” 属性。

type HasName = {
  name: string;
};

type HasAge = {
  age: number;
};

type Person = HasName & HasAge;

let person: Person = {
  name: "Tom",
  age: 20
};

在这个例子中,HasName 和 HasAge 两个类型通过 & 操作符合并为 Person 类型,最终对象包含了这两个类型的所有属性。
所以我们可以知道交叉类型的主要应用场景是合并多个类型。

三、交叉类型与继承的对比

语法上的区别

● 继承:使用 extends 关键字,通常用于类之间的关系。
● 交叉类型:使用 & 操作符,通常用于类型之间的组合。

功能上的区别

● 继承:是对象之间的“父子”关系,子类继承父类的属性和方法,可以对父类的方法进行重写或扩展。
● 交叉类型:是类型的“合体”,多个类型合并成一个新的类型,所有属性都被并入新类型中,没有继承的语义。

结构与行为

● 交叉类型:主要关注于结构的合并,即属性的合并。
● 继承:不仅关注结构,还关注行为的传递,即方法和属性的继承。
灵活性与限制
● 交叉类型:提供了更大的灵活性,允许在不改变原有类型的基础上进行类型合并。
● 继承:提供了行为的封装,但可能会限制类型的灵活性,因为子类必须遵循父类的接口。

四、最佳实践

什么时候使用继承?

当你需要创建一个层次结构,并且子类需要复用父类的行为时。
继承适用于面向对象设计场景,尤其是当你需要表示类之间的层次关系时。继承能够帮助你复用代码,避免重复定义相同的属性和方法,并且能够通过多态机制简化代码设计。

假设我们在做一个用户注册表单。我们可以用继承的方式来构建基础验证类和具体表单验证类。比如有一个基础的 “Validator” 类,它有一些通用的验证方法,然后 “UsernameValidator” 类和 “PasswordValidator” 类继承自它,分别处理用户名和密码的特定验证规则。

class Validator {
  validateLength(input: string): boolean {
    return input.length > 0;
  }
}

class UsernameValidator extends Validator {
  validateUsernameFormat(username: string): boolean {
    // 这里可以添加用户名格式验证逻辑,例如检查是否只包含字母数字字符
    const usernameRegex = /^[a-zA-Z0-9]+$/;
    return usernameRegex.test(username);
  }
}

class PasswordValidator extends Validator {
  validatePasswordStrength(password: string): boolean {
    // 这里可以添加密码强度验证逻辑,比如检查是否包含数字、字母和特殊字符
    const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^\w\d\s:])([^\s]){8,}$/;
    return passwordRegex.test(password);
  }
}

什么时候使用交叉类型?

当你需要合并多个不相关的类型,且不关心它们之间的层次结构时。
交叉类型更适合用于将多个类型合并成一个新的类型,这通常发生在函数、数据结构、或者组合不同功能的场景中。它不涉及继承的“层次关系”,而是将多个类型合并成一个更复杂的类型。

同样的假设我们在做一个用户注册表单。我们可以定义不同的验证规则类型,然后交叉组合成一个最终的表单验证类型。

type UsernameRule = {
  validateUsernameFormat: (username: string) => boolean;
};

type PasswordRule = {
  validatePasswordStrength: (password: string) => boolean;
};

type FormValidation = UsernameRule & PasswordRule;

let formValidation: FormValidation = {
  validateUsernameFormat: (username) => {
    // 具体验证逻辑,如上述用户名格式验证
    const usernameRegex = /^[a-zA-Z0-9]+$/;
    return usernameRegex.test(username);
  },
  validatePasswordStrength: (password) => {
    // 具体验证逻辑,如上述密码强度验证
    const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^\w\d\s:])([^\s]){8,}$/;
    return passwordRegex.test(password);
  }
};

结合使用

在某些情况下,你可以结合使用交叉类型和继承,以获得更大的灵活性和代码复用。

同样是上面的案例,我们可以创建一个 “FullFormValidator” 类,它继承自 “Validator” 类,同时使用交叉类型来组合额外的验证规则。

class FullFormValidator extends Validator {
  constructor() {
    super();
  }

  // 使用交叉类型组合的验证规则
  validationRules: FormValidation = {
    validateUsernameFormat: (username) => {
      const usernameRegex = /^[a-zA-Z0-9]+$/;
      return usernameRegex.test(username);
    },
    validatePasswordStrength: (password) => {
      const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^\w\d\s:])([^\s]){8,}$/;
      return passwordRegex.test(password);
    }
  }

  // 验证整个表单的方法,调用了继承的通用验证和交叉类型组合的特定验证
  validateForm(username: string, password: string): boolean {
    if (!this.validateLength(username) ||!this.validateLength(password)) {
      return false;
    }
    if (!this.validationRules.validateUsernameFormat(username)) {
      return false;
    }
    if (!this.validationRules.validatePasswordStrength(password)) {
      return false;
    }
    return true;
  }
}

这个案例可能有点绕,不过别担心。只要你把它搞清楚了,你就可以正确地理解和有效地使用它们了。

五、常见的坑

继承中的“类型丢失”问题

当你继承一个类并试图覆盖其方法时,如果没有正确使用 super 关键字,可能会导致父类的属性或方法丢失。
示例:

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

class Dog extends Animal {
  breed: string;

  constructor(name: string, breed: string) {
    // 错误:没有调用 super()
    this.name = name; 
    this.breed = breed;
  }
}

此时,name 属性没有被正确初始化,会导致报错。

交叉类型中的“属性冲突”问题

当交叉类型的两个类型有相同的属性时,可能会导致冲突,特别是属性的类型不匹配时。
示例:

type A = { name: string };
type B = { name: number };

type AB = A & B;  // 错误:属性 'name' 的类型不兼容 {name: never}

在交叉类型 A & B 中,name 属性的类型是 string 和 number 的交集,这显然会引发类型错误。

六、 总结

● 继承 是一种表示“是一个”关系的机制,适用于类的层次结构,强调代码复用和多态。
● 交叉类型 是一种类型组合工具,适用于需要合并多个类型的场景,强调灵活性和多功能性。
理解交叉类型和继承的区别对于有效地使用 TypeScript 至关重要。实际开发中,选择使用继承还是交叉类型,通常取决于你的需求。如果你需要构建类之间的层次关系,继承更合适;如果你只是想合并不同的属性,交叉类型则是更好的选择。
通过了解这两者的不同,能够更灵活地运用 TypeScript 的类型系统,让代码更加简洁、灵活和高效。希望这篇文章能够帮助你更好地理解交叉类型与继承的差异,并在实际开发中做出合理的选择~


十六
1 声望0 粉丝

前端学者~