如何更加高效的使用Typescript,辟殊一直坚持认为的就是让 ts 在语法编译检查的能力延伸到运行时和语义上,下面就是一个例子。
问题
type UserId = string
type Address = string
let userId: UserId = 'aUserId'
const address : Address= 'hang zhou'
userId = addres; // 显然不合理
在 Typescript 中在一个变量类型是string ,其语义是一个用户id, userId;另外一个变量类型也是 string,但是其语义是一个地址, address. 显然我们的 ts 代码是允许我们赋值 userid 给 addressid 的;但是这个在语义上是完全不正确的事情。如何让人 typescript 检查这类问题呢?
答案
type UserId = string
& {readonly $_type_$: unique symbol}
function UserId(id:string): UserId{
return id as UserId
}
type Address = string
& {readonly $_type_$: unique symbol}
function Address(add:string): Address{
return add as Address
}
let uid = UserId('111')
const address = Address('hangzhou')
uid = address
// 报错 Type 'Address' is not assignable to type 'UserId'.
这个小技巧利用到了两个知识点
第一点
& {readonly $_type_$: unique symbol}
使用类型合并的方式对技术类型加上额外的业务意义,而且这里使用了 unique symbol
, 让各个业务类型的$_type_$
必然不相同,也就让他们必然不能相互赋值(not assignable)。
第二点
但是这个 unique symbol
其实是不可访问的; 所以是无法直接定义出来这个类型的, 我们直接定义 UserId 就会报错。
const uid2 : UserId = '222222'
// 报错 Type 'string' is not assignable to type 'UserId'.
所以我们就需要一个工具函数,function UserId(id:string): UserId
, 通过强制类型转换来生成对应类型的变量。这个工具函数名可以取任何名字,但是为了让程序的语义更加的简洁,所以直接用和类型同名的函数;这里有个小知识点就是 ts 的编译器是完全有能力区分出你在使用UserId时到底指的是 UserId 类型还是UserId 函数。所以重名是完全 OK的。
完
这个小小的技巧可以在你有类似需要的时候帮你实现这种语义级别的检查,需要的代价也不大,希望能帮助到你。
其实没完:
其实本文的原本的标题是 《如何在基于结构体类型的 Typescript 模拟实现基于类型名义的类型》。看着就拗口是吧,我来一个点点拆解哦。
首先typescript 是一门 structural type 类型的语言,简单点说 ts 用来判断两个变量是否可以相互赋值,是看两个变量对应的类型的属性是否是相互兼容的。比如类型 type A ={ demo : boolean}
,类型一个类型 type B = {demo : boolean }
,因为他们的类型“形状”是一致的,所以他们之间是可以相互赋值的。除了 Tyepscript ,Go 语言也是 structural type 的。
但是还有另外一种类型系统叫 Nominal type system, moninal 的词根 mon 在拉丁语的意思就是名字(name), Nominal type system 意思就是基于类型名的类型系统,上面 A 和 B 的例子,在Nominal type system中就是不能相互赋值的,因为他们完全不同的东西,一个类型名字叫 A,另外一个叫 B,自然相互不能赋值。基于这样类型系统语言有:现在炽手可热的 Rust,老牌的 C++,Java 。类型的实现方式并没有优劣之分,合适的才是最好的。
当然能读到这里小伙伴我相信你一定学到了点新知识,为了更好的鼓励辟殊持续更新,希望大家帮忙转发点赞,扩展阅读链接我也附在这里,一起去锻炼锻炼英语吧。
https://www.wikiwand.com/en/Nominal_type_system
https://www.wikiwand.com/en/Structural_type_system