理解 TypeScript 背后的结构化类型系统

news2024/11/18 4:45:02

前言

你能说清楚类型类型系统类型检查这三个的区别吗?在理解TypeScript的结构化类型系统之前,我们首先要搞清楚这三个概念和它们之间的关系

  • 类型:即对变量的访问限制赋值限制。如 TypeScript 中的原始类型、对象类型、函数类型和字面量类型等类型,当一个变量类型确定后,你不能访问这个类型中不存在的属性或方法,也不能将其他不兼容的类型的变量赋值给该变量
  • 类型系统:本质上是一组规则,其描述如何为变量、函数等结构分配和实施类型。同时还定义了如何判断类型之间的兼容性,也正是我们今天主要讨论的概念
  • 类型检查:是一种能力,一种确保类型遵循类型系统下的类型兼容性的能力

理解它们对理解我们今天要讨论的TypeScript的结构化类型系统很有帮助

类型系统分为结构化类型系统标称类型系统,首先我们来看看它们分别都是什么

什么是结构化类型系统?

基于类型结构进行类型兼容性判断

关键体现在两个类型的比较当中,当两个类型比较时,如果是按照属性和方法是否相同来比较的话就称为结构化类型系统,也叫鸭子类型。

比如下面这个例子:

class Dog {say() {console.log('wang wang wang!')}
}

class Cat {say() {console.log('miao miao miao!')}
}

const invokeSay = (dog: Dog) => {dog.say()
}

const dog = new Dog()
const cat = new Cat()

invokeSay(dog) // wang wang wang!
invokeSay(cat) // miao miao miao! 

虽然invokeSay函数接收的参数类型是Dog,但是由于Cat类型的结构和Dog是一样的(都是只有一个 say 方法),因此会被认为是同一种类型,这就是结构化类型的特点,基于类型结构进行类型兼容性判断

代表语言:C#、Python、Objective-C

什么是标称类型系统?

基于类型名进行兼容性判断

与结构化类型系统相对的叫标称类型系统,它在判断两个类型是否相同时,只看它们的名称是否相同,即便内部结构完全相同也不能认为是同一种类型,比如下面这个例子:

/** @description 人民币 */
type CNY = number

/** @description 美元 */
type USD = number

const CNYCount: CNY = 666
const USDCount: USD = 333

const addCount = (source: CNY, input: CNY) => source + input

addCount(CNYCount, USDCount) 

案例取自掘金小册《TypeScript 全面进阶指南》第 10 节: 结构化类型系统:类型兼容性判断的幕后

在标称类型系统中,这里对于addCount的调用是错误的,尽管CNYUSD类型都是number类型,但由于它们的名称不同,因此被视为是不同类型

代表语言:C++、Java、Rust

结构化类型系统等价于鸭子类型系统吗?

严格意义上两者并不等同

  • 结构化类型系统基于完全的类型结构来判断类型兼容性
  • 鸭子类型系统基于运行时访问的部分来判断类型兼容性

TypeScript 本身并不是在运行时进行类型检查,因此并不严格等价于鸭子类型系统

如何在 TypeScript 中模拟标称类型系统?

由于 TypeScript 的类型系统是结构化类型系统,所以刚刚那个例子在 TypeScript 中是可以正常运行的:

/** @description 人民币 */
type CNY = number

/** @description 美元 */
type USD = number

const CNYCount: CNY = 666
const USDCount: USD = 333

const addCount = (source: CNY, input: CNY) => source + input

addCount(CNYCount, USDCount) 

案例取自掘金小册《TypeScript 全面进阶指南》第 10 节: 结构化类型系统:类型兼容性判断的幕后

这里我们的意图应当是让人民币 CNY 和美元 USD 作为两种不同的类型,但是由于 TypeScript 结构化类型系统的特性,两个类型本质上都是number类型,因此会被认为是同一个类型

而如果是标称类型系统就不会有这个问题,如果我们能在 TypeScript 中模拟实现标称类型系统就符合预期了,那么要怎么模拟呢?

对比结构化类型系统和标称类型系统的特点,只要我们能够在两个完全兼容的结构化类型中加入一个标识符,那么即便这两个类型的结构是兼容的,但由于这个标识符并不相同,因而会被认为是两个不同的类型

利用这个特点我们就可以在 TypeScript 中模拟标称类型系统

以下有两种方式模拟实现标称类型系统:

  • 通过交叉类型实现 – 只能在类型层面上实现,无法在运行时逻辑上实现
  • 通过类实现 – 能兼顾类型层面和运行时逻辑层面

交叉类型实现

我们可以实现一个工具类型Nominal,对传入的泛型参数进行处理,将其和一个特殊的类型进行交叉类型操作,上面提到的标识符就是由这个特殊的类型提供的

nominal.ts

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

export type Nominal<T, U extends string> = T & TagProtector<U>
index.ts
import { Nominal } from './nominal'

/** @description 人民币 */
type CNY = Nominal<number, 'CNY'>

/** @description 美元 */
type USD = Nominal<number, 'USD'>

const CNYCount = 666 as CNY
const USDCount = 333 as USD

const addCount = (source: CNY, input: CNY) => source + input

// 类型“USD”的参数不能赋给类型“CNY”的参数。
// 不能将类型“USD”分配给类型“TagProtector<"CNY">”。
// 属性“__tag__”的类型不兼容。
// 不能将类型“"USD"”分配给类型“"CNY"”。ts(2345)
addCount(CNYCount, USDCount) 

由于 TypeScript 实际运行时还是以 JavaScript 的方式运行的,所以类型代码会被抹除,抹除类型后这个代码仍然能够正常执行

这是因为通过这种方式模拟实现标称类型系统只能在类型层面模拟,实际的运行时并不能起到检查的作用,这时候就要用下面的方案 – 用类模拟实现

类实现

/** @description 人民币 */
class CNY {private __tag__!: voidconstructor(public value: number) {}
}

/** @description 美元 */
class USD {private __tag__!: voidconstructor(public value: number) {}
}

const CNYCount = new CNY(666)
const USDCount = new USD(666)

const addCount = (source: CNY, input: CNY) => source.value + input.value

// 类型“USD”的参数不能赋给类型“CNY”的参数。
// 类型具有私有属性“__tag__”的单独声明。
addCount(CNYCount, USDCount) 

以上两种方式本质都是通过一个非公开的额外属性对类型添加了额外的标识符,从而能够让结构化类型系统将它们判断为不同的类型

总结

相信现在你能够理解什么是结构化类型系统了,正如开头介绍的类型系统的概念中所说,它定义了如何判断类型之间的兼容性

而结构化类型系统对于类型之间兼容性的判断则是基于类型的结构来判断的,只要两个类型的结构上可兼容(如一个类型中的所有属性和方法在另一个类型中都存在),则可以将两个类型视为是兼容的

除此之外,我们还了解了与结构化类型系统对应的标称类型系统,并且了解到如何在TypeScript中模拟实现标称类型系统,让我们对结构化类型系统有更深刻的理解

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/185957.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python代码实现批量yunfile文件下载

建议下载文件大小不要超过1M吧,超过的话,把等待下载时间加大点 —>说明: 使用python2.7+selenium+chrome v49+百度ocr识别,基本上pip install 相关的程序,源码就可以直接运行了,exe文件也编译了,去别的电脑运行感觉有各种问题,大家自己用源码跑吧,最新chrome版本的…

如何实现报表集成?(一)

报表需求在每个企业都是“刚需”&#xff0c;而报表的应用又是其中的关键之一&#xff0c;并不是说报表开发出来就万事大吉了&#xff0c;怎么用、怎么用得好&#xff0c;也是用户非常关注的。在这个话题中&#xff0c;报表的集成是个绕不过去的坎&#xff0c;如何通过集成&…

机器学习之参数学习

下述内容为课程小结 定义 参数估计的方法包括经验风险最小化、结构风险最小化、最大似然估计、最大后验估计。 参数估计用于学习模型参数&#xff0c;以达到最优的目的&#xff0c;如线性回归的模型参数 经验风险最小化 对于输入的待处理数据格式为(x&#xff0c;y){(x&…

Plant Simulation热力图工具V2.1全新发布

在做AGV路径规划或人员路径规划时&#xff0c;如果配套热力图&#xff0c;是可以非常方便的分析出相应位置的热点情况&#xff0c;决策人员可以更加方便的确定方案修改思路&#xff0c;比如下图可以非常清晰地看到AGV的停顿位置和路口的使用情况。较早之前&#xff0c;波哥开发…

Vue3+Vite+Element-Plus实现CRUD常见表单项目

效果有 查询&#xff0c;增加&#xff0c;表格&#xff0c;删除&#xff0c;编辑 其实CRUD&#xff0c;就是一个管理项目最常见的功能 C增加 (Create) R读取 (Read) U更新 (Update) D删除 (Delete) 一、创建项目 vue3用vite创建项目 1 对应路径cmd 输入 npm create vitelates…

C 语言零基础入门教程(十八)

C 输入 & 输出 当我们提到输入时&#xff0c;这意味着要向程序填充一些数据。输入可以是以文件的形式或从命令行中进行。C 语言提供了一系列内置的函数来读取给定的输入&#xff0c;并根据需要填充到程序中。 当我们提到输出时&#xff0c;这意味着要在屏幕上、打印机上或…

SSM框架整合(Spring+SpringMVC+MyBatis)

一、创建MAVEN工程 二、导入pom依赖 <dependency><groupId>com.mchange</groupId><artifactId>c3p0</artifactId><version>0.9.5.2</version></dependency><!-- https://mvnrepository.com/artifact/commons-logging/comm…

项目错误排查

项目运行不起来&#xff0c;先观察表现&#xff0c;就像中医所讲的望。复现&#xff0c;了解触发问题的时机和过程。在哪个步骤&#xff0c;哪个接口出了问题。闻问切浏览器f12&#xff0c;根据请求参数和响应码判断问题出在前端还是后端。查看错误日志&#xff0c;一般写的还是…

LeetCode011之盛最多水的容器(相关话题:双指针,逻辑分析)

题目描述 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明&#xff1a;你不能倾斜容…

小程序 --- 收货地址 --- 定位功能

一、整体功能如下图二、功能点定位选择自己的位置手动选择省市县后输入详细地址关键词搜索地址&#xff0c;然后进行选择三、如何实现定位采用小程序API wx.chooseLocation 调出地图选择位置 API wx.chooseLocation 详细说明根据地址解析出省市县以及详细地址及经纬度代码实现c…

【Linux】-- 进程概念

目录 一、进程概念 二、PCB 1.什么是PCB 2.什么是task_struct 3.task_struct包含内容 三、task_struct内容详解 1.查看进程 &#xff08;1&#xff09;通过系统目录查看 &#xff08;2&#xff09;通过ps命令查看 &#xff08;3&#xff09;通过top命令查看 &…

Mysql自定义变量在递归遍历中的妙用

借着在解决一个递归查询父目录的问题&#xff0c;学习了一下mysql变量的用法&#xff0c;在某些场景下这种解法还是比较有特效的&#xff0c;下面具体来聊一下场景&#xff0c;同时也会详细分析下mysql自定义变量的用法 场景&#xff1a; 获取从树的根节点到叶子节点的全路径&…

图解基于UDS的Flash BootLoader

图解基于UDS的Flash BootLoader一、为什么要搞Bootloader&#xff1f;为什么要基于UDS搞Bootloader二、Bootloader应支持的UDS服务三、Bootloader——三段式(1) 预编程阶段(2) 主编程阶段(3)后编程状态四、BootLoader的启动顺序与转换流程五、问题点疑问点 Q:图中的烧写顺序是…

点云 3D 目标检测 - RangeDet(ICCV 2021)

点云 3D 目标检测 - RangeDet&#xff08;ICCV 2021&#xff09;摘要1. 引言2. 相关工作3. 距离视图表示的回顾4. 方法4.1 距离条件金字塔4.2 元核卷积4.3 加权非最大抑制4.4 距离视图中的数据增强4.5 体系结构5. 实验5.1 元核卷积的研究5.2 距离条件金字塔的研究5.3 加权非最大…

基于android的有声听书系统

需求信息&#xff1a; 1&#xff1a;注册登录&#xff1a;未注册用户首先进行账号注册&#xff0c;注册成功后进行登录&#xff0c;已注册用户直接输入账号密码进行登录&#xff0c;登录成功后进入主页面。 2&#xff1a;主页面&#xff1a;通过左右滑动可以实现对推荐界面、订…

Activity7工作流介绍_和BPM语言介绍---工作流工作笔记003

只说经典,重要的部分,节省学习时间,用最快的速度学习掌握 看一个简单审批流程,要知道工作流是基于状态驱动的,就是比如,状态有: 0 已创建 1 已提交 2 已部门经理审核 3 已总经理审核 4 审核通过 5 审核拒绝 根据这些状态来做驱动. 这里需要用到流程引擎,常见的比如有Drools规…

不看后悔,一文入门Go云原生微服务

文章目录打好基础微服务框架对比简单横评各个框架微服务概念软件架构演进史简单理解微服务的好处go-micro概述构成组件Go MicroAPISidecarWebCLIBot总结Go Micro组件架构Registry注册中心Selector负载均衡Broker事件驱动&#xff1a;发布订阅Transport消息传输总结快速入门准备…

使用Java8优化模板方法模式

目录 前言 以前的模板方法 Java 8 的函数式编程 Java 8以后的模板方法 总结 前言 我们在日常开发中&#xff0c;经常会遇到类似的场景&#xff1a;当要做一件事儿的时候&#xff0c;这件事儿的步骤是固定好的&#xff0c;但是每一个步骤的具体实现方式是不一定的。 通…

网络 随笔 2-linux的三种网络模式

0. 前面的科普对操作系统网络的理解还有帮助的 简单点&#xff0c;linux三种网络模式 linux中的三种网络模式 1. bridge 物理网卡使用虚拟网桥作为虚拟交换机的输入物理机以及虚拟网卡接入这个虚拟交换机虚拟网卡与物理网卡处于一个网段下(网关与DNS 一致) 2. NAT 虚拟的N…

删除的文件怎么恢复?误删文件恢复,就使用这些方法!

电脑里面保存着很多文件&#xff0c;为了让电脑更整洁&#xff0c;我们一般都会定期清理不必要的数据。在清理过程中&#xff0c;出现文件被误删&#xff0c;我们该怎么办&#xff1f;误删文件恢复&#xff0c;方法就看下面三个&#xff1a;注册表恢复、回收站恢复、软件恢复。…