typescript:结构化类型系统

一、结构化类型系统

(1)TypeScript 比较两个类型并非通过类型的名称,而是比较这两个类型上实际拥有的属性与方法。

(2)结构类型的别称鸭子类型(Duck Typing,这个名字来源于鸭子测试(Duck Test。其核心理念是,如果你看到一只鸟走起来像鸭子,游泳像鸭子,叫得也像鸭子,那么这只鸟就是鸭子

(3)面向对象编程中的里氏替换原则也提到了鸭子测试:如果它看起来像鸭子,叫起来也像鸭子,但是却需要电池才能工作,那么你的抽象很可能出错了

(4) 在比较对象类型的属性时,同样会采用结构化类型系统进行判断。而对结构中的函数类型(即方法)进行比较时,同样存在类型的兼容性比较。

(5)这就是结构化类型系统的核心理念,即基于类型结构进行判断类型兼容性。结构化类型系统在 C#、Python、Objective-C 等语言中都被广泛使用或支持。

(6)除了基于类型结构进行兼容性判断的结构化类型系统以外,还有一种基于类型名进行兼容性判断的类型系统,标称类型系统。

class Cat {
  eat() { }
}

class Dog {
  eat() { }
}

function feedCat(cat: Cat) { }
//不会报错
feedCat(new Dog())
class Cat {
  meow() { }
  eat() { }
}

class Dog {
  eat() { }
}

function feedCat(cat: Cat) { }

// 报错!
feedCat(new Dog())

二、标称类型系统

(1)标称类型系统(Nominal Typing System)要求,两个可兼容的类型,其名称必须是完全一致的

type USD = number;
type CNY = number;

const CNYCount: CNY = 200;
const USDCount: USD = 200;

function addCNY(source: CNY, input: CNY) {
  return source + input;
}

addCNY(CNYCount, USDCount)

1、在结构化类型系统中,USD 与 CNY (分别代表美元单位与人民币单位)被认为是两个完全一致的类型,因此在 addCNY 函数中可以传入 USD 类型的变量。这就很离谱了,人民币与美元这两个单位实际的意义并不一致,怎么能进行相加?

2、在标称类型系统中,CNY 与 USD 被认为是两个完全不同的类型,因此能够避免这一情况发生。

(2)在《编程与类型系统》一书中提到,类型的重要意义之一是限制了数据的可用操作与实际意义,这一点在标称类型系统中的体现要更加明显。比如,上面我们可以通过类型的结构,来让结构化类型系统认为两个类型具有父子类型关系,而对于标称类型系统,父子类型关系只能通过显式的继承来实现,称为标称子类型(Nominal Subtyping)

class Cat { }
// 实现一只短毛猫!
class ShorthairCat extends Cat { }

 C++、Java、Rust 等语言中都主要使用标称类型系统。

三、在 TypeScript 中模拟标称类型系统

(1) 再看一遍这句话:类型的重要意义之一是限制了数据的可用操作与实际意义。这往往是通过类型附带的额外信息来实现的(类似于元数据),要在 TypeScript 中实现,其实我们也只需要为类型额外附加元数据即可,比如 CNY 与 USD,我们分别附加上它们的单位信息即可,但同时又需要保留原本的信息(即原本的 number 类型)。

1、通过交叉类型的方式来实现信息的附加:

export declare class TagProtector<T extends string> {
  protected __tag__: T;
}

export type Nominal<T, U extends string> = T & TagProtector<U>;

2、在这里我们使用 TagProtector 声明了一个具有 protected 属性的类,使用它来携带额外的信息,并和原本的类型合并到一起,就得到了 Nominal 工具类型。 

export type CNY = Nominal<number, 'CNY'>;

export type USD = Nominal<number, 'USD'>;

const CNYCount = 100 as CNY;

const USDCount = 100 as USD;

function addCNY(source: CNY, input: CNY) {
  return (source + input) as CNY;
}

addCNY(CNYCount, CNYCount);

// 报错了!
addCNY(CNYCount, USDCount);

3、这一实现方式本质上只在类型层面做了数据的处理,在运行时无法进行进一步的限制。 

(2) 从逻辑层面入手进一步确保安全性

class CNY {
  private __tag!: void;
  constructor(public value: number) {}
}
class USD {
  private __tag!: void;
  constructor(public value: number) {}
}

1、相应的,现在使用方式也要进行变化:

const CNYCount = new CNY(100);
const USDCount = new USD(100);

function addCNY(source: CNY, input: CNY) {
  return (source.value + input.value);
}

addCNY(CNYCount, CNYCount);
// 报错了!
addCNY(CNYCount, USDCount);

2、通过这种方式,我们可以在运行时添加更多的检查逻辑,同时在类型层面也得到了保障。

(3)这两种方式的本质都是通过额外属性实现了类型信息的附加,从而使得结构化类型系统将结构一致的两个类型也判断为不可兼容。

(4) 在 TypeScript 中我们可以通过类型或者逻辑的方式来模拟标称类型,这两种方式其实并没有非常明显的优劣之分,基于类型实现更加轻量,你的代码逻辑不会受到影响,但难以进行额外的逻辑检查工作。而使用逻辑实现稍显繁琐,但你能够进行更进一步或更细致的约束。

四、总结

(1) TypeScript 的结构化类型系统是基于类型结构进行比较的,而标称类型系统是基于类型名来进行比较的。

(2)在 TypeScript 中,可以通过为类型附加信息的方式,从类型层面或者逻辑层面出发去模拟标称类型系统。

(3)类型、类型系统与类型检查

  • 类型:限制了数据的可用操作、意义、允许的值的集合,总的来说就是访问限制赋值限制。在 TypeScript 中即是原始类型、对象类型、函数类型、字面量类型等基础类型,以及类型别名、联合类型等经过类型编程后得到的类型。
  • 类型系统:一组为变量、函数等结构分配、实施类型的规则,通过显式地指定或类型推导来分配类型。同时类型系统也定义了如何判断类型之间的兼容性:在 TypeScript 中即是结构化类型系统。
  • 类型检查:确保类型遵循类型系统下的类型兼容性,对于静态类型语言,在编译时进行,而对于动态语言,则在运行时进行。TypeScript 就是在编译时进行类型检查的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值