浅谈 TS 标称类型介绍及社区实现

Posted 阿里巴巴淘系技术团队官网博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈 TS 标称类型介绍及社区实现相关的知识,希望对你有一定的参考价值。

本文将以稍偏门的视角来看待 TypeScript 的类型系统,主要介绍标签类型是什么,以及 TS 社区都有哪些实现手段。

前言

有位大神说过"程序是类型的证明",我看不懂,但我大受震撼。为了以后能看懂哪怕一点点,我决定记录下类型相关的所学所悟。

《浅谈 TS 标称类型》系列将以稍偏门的视角来看待 TypeScript 的类型系统,实际用途不大,但自觉有趣。本文是该系列的开篇文章,主要介绍标签类型是什么,以及 TS 社区都有哪些实现手段。

什么是标称类型系统(nominal type system)

先通俗地理解下,举个例子,userId = 123bookId = 34都是数字,但两者用于不同的场景,希望用不同类型 UserID 和 BookID 来表示,且不能互换。像这样,数据的值本身没什么区别,安上不同名字就是不同类型,这就是标称类型系统(nominal type system)。也就是说,标称类型系统中,两个变量是否类型兼容(可以交换赋值)取决于这两个变量显式声明的类型名字是否相同。

与之相对的是结构类型系统(structural type system),类型兼容只取决于实际结构是否相同,与类型名字无关。比如:定义Point类型包含xy两个数字,rect = x: 33, y: 3, width: 30, height: 80 的结构满足Point的定义,就属于Point类型。简单理解,结构类型系统中,结构或者说形状相同的两个值,它们的类型是兼容的,可以交换赋值。

更严格的定义可以看下Type system - Wikipedia的说明。

除了上面的 UserID 和 BookID 的例子,标称类型还有其他常见的应用场景,比如:区分不同的字符串(正则表达式、html模版、文件路径等),表达不同单位的量纲(不同币种的金额、css各种长度单位)等。这些会在后续文章再展开说明,届时也会列举下标称类型常见的错误用法。

TS 是标称类型系统吗

不是。TS 是结构类型系统(structural type system),基于结构/形状检查类型,而非类型的名字。

One of TypeScript’s core principles is that type checking focuses on the shape that values have. This is sometimes called “duck typing” or “structural typing”.

TypeScript: Documentation - TypeScript for javascript Programmers

上面是TS官方文档的说明,里面还举了一些例子,可以先看看加深理解。

TS 可以实现标称类型吗

可以(不然这篇文章写到这里就要结束了)。TS 目前不支持显式声明标称类型,也没有计划支持,2014年的提案Support some non-structural (nominal) type matching · Issue #202 到现在还是Open状态。不过社区有不少方案,可以基于现有 TS 的能力一定程度上实现标称类型,整理如下。

TS 实现标称类型的各种手段

为了方便,下面都用 CNY、USD 币种来示例,类型检查用下面两个方法测试。

function buyPekingDuck(money: CNY)  // 只能用 CNY 买北京烤鸭
function buyCocaCola(money: USD)  // 只能用 USD 买可口可乐

为了术语一致,下文统一用下列中文字词(若与习惯的表述不一致,请以英文单词为准)

  1. Type Annotation: 类型声明  变量: 类型 ,比如 let yuan: CNY

  2. Type Assertion: 类型断言 表达式 as 类型,比如 12 as CNY

  3. Type Compatibility: 类型兼容,指一个类型可以赋值给另一个类型

  4. Type Infer: 类型推断,指 TS 根据上下文推断变量或值的类型,比如 let a = 12 推断 a 是 number

  5. Primitive Type: 原始类型,指stringnumber 和 boolean


▐  定义私有属性的类 Class with a private property


class CNY 
  private __brand: void
  constructor(public value: number) 

class USD 
  private __brand: void
  constructor(public value: number) 


// 用例
const yuan = new CNY(12)
const dollar = new USD(5)

// 类型安全
buyPekingDuck(dollar) // Argument of type 'USD' is not assignable to parameter of type 'CNY'.
buyCocaCola(yuan) // Argument of type 'CNY' is not assignable to parameter of type 'USD'.

这个方法利用了 TS 对 private/protected 的特殊处理——判断类型兼容时,如果其中一个包含私有属性,则另一个必须包含来自同一个类声明的相同私有属性。yuan 和 dollar 都有私有属性__brand,但来自不同的类声明(分别是CNY 和 USD),所以它们类型不兼容。

优点:不需要类型声明(type annotation),也不需要类型断言(type assertion),TS 能推导出对应的类型(type infer)。

缺点:冗余的类声明,多了一层 value 的结构,不能支持原始类型,需要额外的序列化处理。

推荐度:不推荐。除非本来就是用类实现,而且要严格区分字段相同、语义不同的两个类型,才考虑该方案。

▐  包含字面量类型

type CNY = 
  currency: 'CNY',
  value: number,

type USD = 
  currency: 'USD',
  value: number,


// 用例
const yuan: CNY =  currency: 'CNY', value: 12 
const dollar: USD =  currency: 'USD', value: 5

// 类型安全
buyPekingDuck(dollar) // Argument of type 'USD' is not assignable to parameter of type 'CNY'.
buyCocaCola(yuan) // Argument of type 'CNY' is not assignable to parameter of type 'USD'.

加入不同的字面量类型(literal type)来定义 type 或 interface,因为不同字面量是不同类型,所以组合后的类型也不同。


优点:语义清晰,理解直观,条件判断能实现类型收窄(type narrowing)。

缺点:多了一层 value 的结构,不能支持原始类型,需要额外的序列化处理。

推荐度:看情况。如果本来有结构,而且用于区分的字面量有对应的语义,可以用该方法。

▐  枚举类intersection

enum CNYBrand  _brand 
type CNY = number & CNYBrand

enum USDBrand  _brand 
type USD = number & USDBrand

// 用例
const yuan = 12 as CNY
const dollar = 5 as USD

// 类型安全
buyPekingDuck(dollar) // Argument of type 'USDBrand' is not assignable to parameter of type 'CNYBrand'.
buyCocaCola(yuan) // Argument of type 'CNYBrand' is not assignable to parameter of type 'USDBrand'.

枚举定义了 _brand ,TS会认为是非空数字枚举,两个枚举不兼容,与数字类型交集后就是不同类型。

注意,字符串不能这么用,string & CNYBrand的结果是never。枚举需要定义为 _brand: '',让TS认为是非空字符串枚举,才能跟字符串类型取交集。

优点:无,勉强要说的话,类型断言的 as Xxx 可读性还行。

缺点:需要类型断言,有额外的枚举定义,会生成多余的js代码,数字和字符串类型用法不一样,不支持其他原始类型(布尔类型)。

推荐度:不推荐。为了标称类型增加额外运行损耗,不值得。

▐  unique symbol

type CNY = number & 
  readonly brand: unique symbol


type USD = number & 
  readonly brand: unique symbol


// 用例
const yuan = 12 as CNY
const dollar = 5 as USD

// 类型安全
buyPekingDuck(dollar) // Argument of type 'USD' is not assignable to parameter of type 'CNY'.
buyCocaCola(yuan) // Argument of type 'CNY' is not assignable to parameter of type 'USD'.

TS 里每个 unique symbol 声明都是完全独立的唯一标识,互相不兼容。作为属性加到类型中需要用readonly修饰。

优点:类型定义部分无差异,不用费心思,无额外的结构,运行时无消耗。

缺点:需要类型断言,关键字较多(uniquereadonly),不能用范型。

推荐度:推荐。不会生成额外代码,其唯一性确保类型不会重复。

▐  brand interface

interface CNY extends Number 
  _CNYBrand: string;

interface USD extends Number 
  _USDBrand: string;


// 用例
const yuan: CNY = 12 as any
const dollar: USD = 5 as any

// 类型安全
buyPekingDuck(dollar) // Argument of type 'USD' is not assignable to parameter of type 'CNY'.
buyCocaCola(yuan) // Argument of type 'CNY' is not assignable to parameter of type 'USD'.

用interface 扩展增加互不相同的_xxxBrand变成不同的类型,破坏类型兼容。TS 的源码也使用了该方案。

优点:支持基本类型,没用到黑魔法,无额外的结构,运行时无消耗。

缺点:需要类型声明或类型断言,且需要过 any 一道。

推荐度:非常推荐。大部分需要标称类型的场景不会直接指定类型,缺点可接受,优先考虑该方案。

▐  brand type intersection

type CNY = number & 
  _CNYBrand: string;

type USD = number & 
  _USDBrand: string;


// 用例
const yuan: CNY = 12 as any
const dollar: USD = 5 as any

// 类型安全
buyPekingDuck(dollar) // Argument of type 'USD' is not assignable to parameter of type 'CNY'.
buyCocaCola(yuan) // Argument of type 'CNY' is not assignable to parameter of type 'USD'.

同上,只不过 interface extend 改成等价的 type intersection,即,用类型交集增加互不相同的_xxxBrand变成不同的类型,破坏类型兼容。

优点:支持基本类型,没用到黑魔法,无额外的结构,运行时无消耗。

缺点:需要类型声明或类型断言,且需要过 any 一道。

推荐度:非常推荐。同上,大部分需要标称类型的场景不会直接指定类型,缺点可接受,优先考虑该方案。

上面列举了社区常见的标称类型实现方法,其中个人最推荐的是 brand interface 以及等价的 brand type intersection,原理简单易懂,没有黑魔法,适合绝大多数使用场景,也是 TS 官方源码里在用的方法,值得优先考虑。

后记

本文简单介绍了标称类型是什么,以及 TS 中如何实现。除了本文提到的这些方法外,网上还能找到很多标称类型的实现手段,它们各有优劣,适用场景也有差异,而且随着 TS 升级,有些方法已经失效了,不熟悉的话可能会难以抉择,故没有收录到文章中。

本系列后续文章会从实现原理进一步剖析这些方法,了解其背后的机制,并结合实际使用场景来辨析,争取知其然,知其所以然。

团队介绍

大淘宝技术—行业工作台前端团队,有一群热爱技术,期望用技术推动业务的小伙伴。服务于数千运营和千万商家,打造高效、稳定、好用的下一代数智化操作系统,让运营&商家能更轻松、更快捷,给消费者更好的购物体验。

团队在建设和探索的核心技术有:新一代无代码研发产品Orca,包括需求结构化、领域物料、无代码搭建、服务标准化&编排等细分技术方向;安全生产&用户体验产品盾山,包括监控预警、自动化测试、代码扫描、灰度发布、问题诊断、体验分析等方向;数据化运营技术,包括数据可视化、数据指标分析、策略驱动运营等技术探索方向。

期待一起参与加入行业工作台的建设~

✿  拓展阅读 

作者|亦森

出品|阿里巴巴新零售淘系技术

以上是关于浅谈 TS 标称类型介绍及社区实现的主要内容,如果未能解决你的问题,请参考以下文章

浅谈typescript及语法

浅谈typescript及语法

浅谈typescript及语法

TypeScript学习笔记——TS类型/高级用法及实战优缺点

TypeScript学习笔记——TS类型/高级用法及实战优缺点

TypeScript学习笔记——TS类型/高级用法及实战优缺点