浅谈 TS 标称类型介绍及社区实现
Posted 阿里巴巴淘系技术团队官网博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈 TS 标称类型介绍及社区实现相关的知识,希望对你有一定的参考价值。
本文将以稍偏门的视角来看待 TypeScript 的类型系统,主要介绍标签类型是什么,以及 TS 社区都有哪些实现手段。
前言
有位大神说过"程序是类型的证明",我看不懂,但我大受震撼。为了以后能看懂哪怕一点点,我决定记录下类型相关的所学所悟。
《浅谈 TS 标称类型》系列将以稍偏门的视角来看待 TypeScript 的类型系统,实际用途不大,但自觉有趣。本文是该系列的开篇文章,主要介绍标签类型是什么,以及 TS 社区都有哪些实现手段。
什么是标称类型系统(nominal type system)
先通俗地理解下,举个例子,userId = 123
、bookId = 34
都是数字,但两者用于不同的场景,希望用不同类型 UserID
和 BookID
来表示,且不能互换。像这样,数据的值本身没什么区别,安上不同名字就是不同类型,这就是标称类型系统(nominal type system)。也就是说,标称类型系统中,两个变量是否类型兼容(可以交换赋值)取决于这两个变量显式声明的类型名字是否相同。
与之相对的是结构类型系统(structural type system),类型兼容只取决于实际结构是否相同,与类型名字无关。比如:定义Point
类型包含x
、y
两个数字,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 买可口可乐
为了术语一致,下文统一用下列中文字词(若与习惯的表述不一致,请以英文单词为准)
Type Annotation: 类型声明
变量: 类型
,比如let yuan: CNY
Type Assertion: 类型断言
表达式 as 类型
,比如12 as CNY
Type Compatibility: 类型兼容,指一个类型可以赋值给另一个类型
Type Infer: 类型推断,指 TS 根据上下文推断变量或值的类型,比如
let a = 12
推断a
是number
Primitive Type: 原始类型,指
string
,number
和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
修饰。
优点:类型定义部分无差异,不用费心思,无额外的结构,运行时无消耗。
缺点:需要类型断言,关键字较多(unique
和readonly
),不能用范型。
推荐度:推荐。不会生成额外代码,其唯一性确保类型不会重复。
▐ 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学习笔记——TS类型/高级用法及实战优缺点