Skip to content

名义类型 (Nominal Typing)

名义类型系统(Nominal Typing)与结构化类型系统(Structural Typing)相对,是另一种类型检查机制。在名义类型系统中,两个类型的兼容性不仅仅取决于它们的结构(即属性和方法),还取决于它们的名字或声明方式。换句话说,即使两个类型具有相同的属性和方法,如果它们的名字不同或者没有显式地声明为继承关系,那么它们就不会被认为是兼容的。

名义类型的工作原理

在名义类型系统中,类型的身份由其名称或声明决定,而不是由其内容或结构决定。这意味着两个独立定义但结构相同类型的对象不会被认为是兼容的,除非它们通过某种方式明确地关联起来,例如通过继承或实现接口。

typescript
// 在 TypeScript 中,这实际上是结构化类型的行为。
// 但在名义类型系统中,以下代码不会工作,因为 Point 和 Coordinates 是不同的类型。
interface Point {
  x: number;
  y: number;
}

interface Coordinates {
  x: number;
  y: number;
}

let point: Point = { x: 10, y: 20 };
// let coordinates: Coordinates = point; // 在名义类型系统中,这是不允许的

名义类型的优势

  • 更强的类型安全:名义类型可以防止意外的类型兼容性,确保只有那些被明确设计为兼容的类型才能互换使用。

  • 更好的意图表达:名义类型可以通过名字清晰地表达设计者的意图,减少因结构相似而导致的误解。

  • 模块化和封装性:名义类型有助于保持模块之间的边界清晰,避免不同模块中的类型意外地互相影响。

名义类型的应用场景

虽然 TypeScript 默认采用的是结构化类型系统,但在某些情况下,你可能希望模拟名义类型的行为。例如:

  1. 品牌化类型:通过添加一个唯一的标记属性来区分不同用途但结构相同的类型。

    typescript
    type Kilometers = number & { __brand: "Kilometers" };
    type Miles = number & { __brand: "Miles" };
    
    function kilometersToMiles(km: Kilometers): Miles {
      return (km * 0.621371) as any as Miles;
    }
    
    const distanceInKm: Kilometers = 5 as any as Kilometers;
    const distanceInMiles = kilometersToMiles(distanceInKm);
  2. 自定义类型保护:使用类型谓词来创建更严格的类型检查,确保只有特定条件下的值才被认为是某个类型。

    typescript
    interface Bird {
      fly(): void;
    }
    
    interface Fish {
      swim(): void;
    }
    
    function isBird(animal: Bird | Fish): animal is Bird {
      return (animal as Bird).fly !== undefined;
    }
    
    function move(animal: Bird | Fish) {
      if (isBird(animal)) {
        animal.fly();
      } else {
        animal.swim();
      }
    }
  3. 品牌化工具库:一些工具库可能会提供品牌化类型的功能,以帮助开发者更好地管理复杂的类型关系。

结构化类型 vs 名义类型

特性结构化类型名义类型
兼容性判断依据对象的结构(属性和方法)类型的名字或声明
灵活性更加灵活,允许结构相同的不同类型兼容较少灵活性,类型必须显式关联才能兼容
类型安全可能导致意外的类型兼容性提供更强的类型安全性
意图表达可能不够明确更清晰地表达设计者的意图
使用场景快速开发、原型设计大型项目、需要强类型安全性的场景

在 TypeScript 中模拟名义类型

尽管 TypeScript 主要使用结构化类型系统,但你可以通过一些技巧来模拟名义类型的行为:

  1. 品牌化类型:如上所述,通过添加一个唯一的标记属性来区分不同用途但结构相同的类型。

  2. 类型谓词:使用类型谓词来创建更严格的类型检查,确保只有特定条件下的值才被认为是某个类型。

  3. 私有字段:利用类中的私有字段来创建名义上的区别,即使这些字段实际上并不存储数据。

    typescript
    class Kilometers {
      private readonly _brand: "Kilometers" = "Kilometers";
      constructor(public value: number) {}
    }
    
    class Miles {
      private readonly _brand: "Miles" = "Miles";
      constructor(public value: number) {}
    }
    
    const distanceInKm = new Kilometers(5);
    // const distanceInMiles: Miles = distanceInKm; // 错误:类型不兼容
  4. 符号(Symbol):使用 JavaScript 的 Symbol 来创建独一无二的键,从而在类型级别上区分不同的对象。

    typescript
    const KilometersBrand = Symbol("Kilometers");
    const MilesBrand = Symbol("Miles");
    
    type Kilometers = number & { [KilometersBrand]: never };
    type Miles = number & { [MilesBrand]: never };
    
    function kilometersToMiles(km: Kilometers): Miles {
      return (km * 0.621371) as any as Miles;
    }
    
    const distanceInKm: Kilometers = 5 as any as Kilometers;
    const distanceInMiles = kilometersToMiles(distanceInKm);

总结

名义类型是一种基于类型名称或声明来进行类型兼容性判断的机制,它提供了更强的类型安全性和意图表达能力。虽然 TypeScript 默认使用结构化类型系统,但在某些情况下,通过品牌化类型、类型谓词、私有字段或符号等技巧,可以模拟名义类型的行为,以满足特定的需求。理解结构化类型和名义类型的区别,并根据具体需求选择合适的类型系统,可以帮助你在编写代码时做出更好的设计决策。

Released under the MIT License.