用好 TypeScript,请再深入一些

news2025/1/17 23:18:31

TypeScript 已经成为前端编程语言的事实标准。但我从大量的 Code Review 和面试经历中发现,真正能深入使用 TypeScript 的开发其实并不多。如果你不知道 ReturnType<T> 的作用和实现,或许这篇文章也适合你。

当然,我们花大量时间去学习一门语言或技术并非为了追求奇技淫巧,而是出于务实的态度。如果你看过大量使用 any 的 TypeScript 的代码,你肯定会感叹“那还不如不用”。在此种情况下,使用 TypeScript 的成本大于它所带来的收益。

如何充分利用好 TypeScript 的语言特性,帮助自己写出更健壮、类型提示更友好的代码?那就需要"再深入一些"。

一个简单的例子

假设有以下的 TypeScript 代码,你会如何添加类型信息呢?

function prop(obj, key) {return obj[key];
} 

上面的函数接收一个对象和一个 key,返回对应的属性值。我们也许可以尝试这样写:

function prop(obj: {}, key: string) {return obj[key];
} 

我们限定了 obj 必须是个对象,key 必须是一个字符串,但返回值呢?假如我们这样使用该函数:

const todo = {id: 1,text: "Buy milk",due: new Date(2016, 11, 31),
};

const id = prop(todo, "id"); // any
const text = prop(todo, "text"); // any
const due = prop(todo, "due"); // any 

因为 JavaScript 是高度动态的语言,我们没办法预知 obj 的具体类型和key的值,也就没办法知道返回值的类型,所以 TypeScript 将返回值推断为 any。如果我们想要得到更精确的返回值类型,那该如何解?

想一想:上面我们用 {} 来约束 obj 必须是一个对象,那你知道在 TypeScript 中,{}objectObject 三种类型的区别吗,它们分别用在什么场景?

keyof 操作符

keyof 是 TypeScript 2.1 版本增加的操作符,我们用它来解决上面的问题。

interface Todo {id: number;text: string;due: Date;
}
type TodoKeys = keyof Todo; // "id" | "text" | "due" 

从上面的示例代码中可以看到,keyof 作用于类型 Todo,可以得到 Todo 中所有 key 的联合类型,即上面的 "id" | "text" | "due"。有了 keyof 的助力,我们可以改写 prop 方法:

function prop<T, K extends keyof T>(obj: T, key: K) {return obj[key];
} 

上面的函数签名中,key 的类型 K 不再是任意的字符串,而是要求必须存在于 obj 对象属性名的联合类型中。

想一想:停下来思考一下 K extends keyof T 这种写法,尝试用自己的话描述它的含义。如果说不清楚,原因很可能是对 extends 关键字的理解不够清晰。

上面的代码中,obj 的类型是 T,key 的类型是 K,那么返回值 obj[key] 的类型会被推断为 T[K],在 TypeScript 中这被称为 Indexed Access Types。现在我们重新通过 prop 函数来访问 todo 的属性:

const todo = {id: 1,text: "Buy milk",due: new Date(2016, 11, 31),
};

const id = prop(todo, "id"); // number
const text = prop(todo, "text"); // string
const due = prop(todo, "due"); // Date 

可以看到,现在返回值类型能被正确推断,而不是笼统的 any 类型。另外,正因为我们限定 K 必须存在于 obj 属性名的联合类型中,所以传入其他 key 值,TS 会报错,代码会更健壮:

const none = prop(todo, 'none');

// [ts] Argument of type 'none' is not assignable to parameter of type '"id" | "text" | "due"'. 

想一想:如何用刚学的这些知识点,来给 JavaScript 语言内置的 Object.entries() 方法补充类型签名?聪明的你可以打开 VSCode 看一看。

第二个简单的例子

你觉得下面的代码存在什么问题?

interface Point {x: number;y: number;
}

interface FrozenPoint {readonly x: number;readonly y: number;
}

function freezePoint(p: Point): FrozenPoint {return Object.freeze(p);
}

const origin = freezePoint({ x: 0, y: 0 });

// Error! Cannot assign to 'x' because it
// is a constant or a read-only property.
origin.x = 42; 

表面上看我们确实实现了想要的效果,我们冻结了一个对象,修改冻结对象的属性会报错,一切似乎都没问题。但真的吗?

至少存在两个缺陷:

1.我们需要定义两个 interface,并且每当 Point 类型有修改,对应的 FrozenPoint 也要修改,这样做不但修改成本高,也很容易遗漏导致出错;
2.对于每一个需要 Object.freeze 的对象,我们都要封装一个类似 freezePoint 这样的函数来得到正确的返回值类型,实在太繁琐。

那解法又是什么?

映射类型

TypeScript 中的映射类型允许你通过映射已有类型的每个属性来创建新的类型。我们直接来看 Object.freeze() 方法的类型签名:

freeze<T>(o: T): Readonly<T>; 

这里的 Readonly<T> 返回类型就是一个映射类型,它的定义如下:

type Readonly<T> = {readonly [P in keyof T]: T[P];
}; 

在讲述过第一个例子后,你对上面代码中的 keyof T,T[P] 写法应该不再陌生,剩余的关键点是属性方括号中的 in 操作符,正是它告诉我们这是一个映射类型。

[P in keyof T]: T[P] 意味着类型 T 的每一个 P 属性类型都应该转换为 T[P] 类型。如果没有 readonly 修饰符,这种转换后的类型和之前是一样的。

想一想:readonly 修饰符可以约束属性是只读的,那你知道 TypeScript 中还有哪些属性修饰符吗?

如果还是不能理解上面的代码,可以看看下面的转换过程(转换过程只是为了解释,并非 TypeScript 中的具体算法):

// 泛型参数 T 我们传入了上面定义的 Point 类型,得到 ReadonlyPoint 类型
type ReadonlyPoint = Readonly<Point>;

// 等价于
type ReadonlyPoint = {readonly [P in keyof Point]: Point[P];
};

// 进一步,我们展开 keyofPoint
type ReadonlyPoint = {readonly [P in "x" | "y"]: Point[P];
};
 
// P 代表了 x 和 y 属性,我们可以分别声明,进而去掉映射类型语法:
type ReadonlyPoint = {readonly x: Point["x"];readonly y: Point["y"];
};

// 最后我们可以用 number 代替 T[P] 形式的查询类型:
type ReadonlyPoint = {readonly x: number;readonly y: number;
};

而这就是我们最初代码中定义的 FrozenPoint 类型。 

从上面的代码中,我们得到的 ReadonlyPoint 和 FrozenPoint 类型是完全一样的。但使用 Readonly 映射类型避免了上文中提到的两个问题。

想一想:Readonly<T> 映射类型是 TS 的内置类型,除此之外,TS 还定义了哪些映射类型?你能整理出所有的内置映射类型并弄明白它们的作用和实现吗?

回到最初的问题

文章开头我们有提到 ReturnType<T>,它的作用和实现是什么?这次我们不卖关子,直接给出代码。

它的用法:

type A = ReturnType<() => string>; // string
type B = ReturnType<() => () => any[]>; // () => any[]
type C = ReturnType<typeof Math.random>; // number
type D = ReturnType<typeof Array.isArray>; // boolean 

它的定义:

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]
) => infer R? R: any; 

从用法中我们可以看到,ReturnType 接收一个函数类型参数,然后推断出该函数类型的返回值类型,比如它推断出 Math.random 函数的返回值类型是 number。

想一想:为什么是 ReturnType<typeof Math.random>,而不是 ReturnType<Math.random> ?

我们再来看看它的实现。乍看之下你可能会觉得很复杂,因为它涉及到了 Typescript 这门语言中最难的部分,也就是所谓的类型编程。Typescript 的类型编程本身是图灵完备的,比如你可以使用三元运算符来决定使用哪个类型。

T extends U ? X : Y 

上面的表达式,在 TS 中被称为条件类型。条件类型使用了熟悉的 … ? … : … 语法,它在 Javascript 中被用于条件表达式。T, U, X 和 Y 代表了任意类型。 其中 T extends U 描述了类型关系测试。如果条件满足,类型 X 被选择,否则类型 Y 被选择。

如果使用人类语言,条件类型可以描述如下:如果类型 T 可以赋值给 U,选择类型 X,否则选择类型 Y。

此时我们重新回过头来看 ReturnType<T> 的实现,是不是更容易理解了?ReturnType 的泛型参数我们约束为函数类型,如果 T 真满足约束,那么我们可以通过 infer 关键字推断函数返回值类型 R,否则就返回 any 类型。

想一想:我们既然可以通过条件类型推断函数的返回值类型,那是不是同样可以推断函数的入参类型呢?聪明的你可以在 VSCode 里试试 Parameters<T> 类型。

既然我们已经知道了映射类型和条件类型,那么两者结合起来怎么样?

type NonNullablePropertyKeys<T> = {[P in keyof T]: null extends T[P] ? never : P;
}[keyof T]; 

你能按照第二个例子中的推导过程,推导以下代码中 NonNullableUserPropertyKeys 的最终类型吗?

type User = {name: string;email: string | null;
};

type NonNullableUserPropertyKeys = NonNullablePropertyKeys<User>; 

想一想: NonNullablePropertyKeys<T> 的定义中用到了 never 关键字,你知道它的含义吗?另外,你能用自己的话说清楚 any 和 unknown 类型的区别吗?

结语

这篇文章并不想就 TypeScript 的某个主题深入讲解,而是提出这样一个事实:TypeScript 的类型使用远比想象中复杂。为什么会这样?因为 JavaScript 是动态的,很多类型只能通过文中提到一些方法来描述。

当然并不是说你掌握了 keyof 操作符、映射类型、条件类型就万事大吉了,如果你真的想使用好 TypeScript,必须再深入一些,彻底掌握 TS 的类型编程。

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



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

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

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

相关文章

tinode客户端安卓版编译手账

前一阵子我自己架设了一个tinode的IM服务器, web直接可以运行 但是安卓版本的一直报错&#xff0c; 具体信息为&#xff1a; No subjectAltNames on the certificate match 问了作者&#xff0c;作者竟然把我的问题直接删除了&#xff0c;还是自己调试代码吧。毕竟源码面前…

两年CRUD,没料到我这渣二本,备战两个月面试阿里,居然侥幸拿下P6的offer

对于很多没有学历优势的人来说&#xff0c;面试大厂是非常困难的&#xff0c;这对我而言&#xff0c;也是一样&#xff0c;出身于二本&#xff0c;原本以为就三点一线的生活度过一生&#xff0c;直到生活上的变故&#xff0c;才让我有了新的想法和目标&#xff0c;因此我这个二…

fl studio21版本如何更新FL最新版升级教程

2022年12月7日晚&#xff0c;全球知名的音乐创作软件&#xff0c;FL Studio正式推出最新21版&#xff0c;为原创音乐人提供更好用的DAW&#xff08;数字音乐工作站&#xff09;工具。 FL Studio中文已上线21新版 FL Studio国人也叫它水果编曲软件&#xff0c;是一款有着20多年…

Java——布隆过滤器

在上一篇博客中讲到位图是用来判定一个正整数是否存在的。对于一个负数&#xff0c;我们可以统一规定让他们加上一个数&#xff0c;变成正数&#xff0c;然后用位图的方式存储。但是对于字符串&#xff0c;我们就没办法存储了。因此发明了布隆过滤器 概念 对于网络上很多需要…

计算机毕设Python+Vue校园新闻发布系统(程序+LW+部署)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【从零开始学习深度学习】26.卷积神经网络之AlexNet模型介绍及其Pytorch实现【含完整代码】

目录1. AlexNet模型1.1 AlexNet与LeNet的区别1.2 简化的AlexNet实现1.3 各层输出形状详解2. 读取数据3. 模型训练4. 总结上一篇文章中我们了解到神经网络可以直接基于图像的原始像素进行分类&#xff0c;这种称为端到端&#xff08;end-to-end&#xff09;的方法可以节省很多中…

腾讯实践:从推荐模型的基础特点看大规模推荐类深度学习系统的设计

省时查报告-专业、及时、全面的行研报告库省时查方案-专业、及时、全面的营销策划方案库【免费下载】2022年11月份热门报告盘点腾讯新闻信息流推荐技术实践.pdf推荐系统在腾讯游戏中的应用实践.pdf基于深度学习的个性化推荐系统实时化改造与升级.pdf推荐技术在vivo互联网商业化…

Zabbix与乐维监控对比分析(四)——告警管理篇

在前面发布的Zabbix与乐维监控对比分析文章中&#xff0c;我们评析了二者在架构与性能、Agent管理、自动发现、权限管理、对象管理等方面的差异。接下来让我们一起看看二者在告警管理方面的差异。 告警管理是所有IT监控平台最重磅的功能之一&#xff0c;也是评判一个监控平台好…

cad2010怎么隐藏标注尺寸,cad2007怎么隐藏标注尺寸

1、CAD2007怎么隐藏所有的标注尺寸? 1、在"查看器"菜单面板中隐藏的工具有"线宽"、"测量"、"文本"三种工具,可用于隐藏或显示CAD图中的线条宽度、测量尺寸和文本内容。 2、点击选择"测量"工具,将尺寸内容的CAD图隐藏起来。…

JavaScript-BOM

&#x1f496;通过看视频教程和红宝书浅浅的写下一些关于BOM的笔记 红宝书知识系统全面&#xff0c;精炼。大概是因为太干货了&#xff0c;涉及的知识点太多&#xff0c;所以我选择看着简单的视频教程&#xff0c;同时打开红宝书。笔记的内容以红宝书为基准。 window对象 BOM的…

艾美捷内皮细胞生长添加剂解决方案

内皮细胞生长添加剂是一种培养基补充物&#xff0c;旨在体外优化人原代微血管内皮细胞的生长。这是一种无菌浓缩&#xff08;100X&#xff09;溶液&#xff0c;含有培养正常人微血管内皮细胞所需的生长因子、激素和蛋白质。该补充剂的配制&#xff08;定量和定性&#xff09;旨…

Linux下的多线程编程

线程&#xff08;thread&#xff09;技术早在60年代就被提出&#xff0c;但真正应用多线程到操作系统中去&#xff0c;是在80年代中期&#xff0c;solaris是这方面的佼佼者。传统的Unix也支持线程的概念&#xff0c;但是在一个进程&#xff08;process&#xff09;中只允许有一…

基于java+springmvc+mybatis+vue+mysql的教资考前指导系统

项目介绍 对于本教资考前指导系统的设计来说&#xff0c;系统开发主要是采用java语言技术&#xff0c;后端采用springboot框架&#xff0c;前端采用vue技术&#xff0c;在整个系统的设计中应用MySQL数据库来完成数据存储&#xff0c;具体根据教资考前指导系统的现状来进行开发…

Metasploit Framework简介

没有框架渗透测试者的困扰 ● 需要掌握数百个工具软件&#xff0c;上千个命令参数&#xff0c;实在记不住 ● 新出现的漏洞PoC/EXP有不同的运行环境要求&#xff0c;准备工作繁琐 ● 大部分时间都在学习不同工具的使用习惯&#xff0c;如果能同意就好了 ● Metasploit能解决以上…

pyinstaller遇到的问题

我到底看看能有多少问题&#xff0c;真的烦死我了&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#…

[附源码]Python计算机毕业设计公交电子站牌管理系统软件Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

HAProxy走私漏洞

HAProxy走私漏洞 JFrog安全研究团队发布了一个HAProxy的严重漏洞的信息。HAProxy是一个使用C语言编写的自由及开放源代码软件&#xff0c;其提供高可用性、负载均衡&#xff0c;以及基于TCP和HTTP的应用程序代理。 参考文章&#xff1a;https://jfrog.com/blog/critical-vulne…

虚拟生产、交付、体验,元宇宙技术对供应链的深远影响#低碳生活

#背景自新冠肺炎疫情爆发以来&#xff0c;元宇宙增长速度加快&#xff0c;也推动了对远程工作工具的前所未有的需求。目前全球元宇宙市场估值高于 1000 亿美元&#xff0c;据预计&#xff0c;到 2029 年&#xff0c;预计年均增长 47 %&#xff0c;达到 15270 亿美元。#改造供应…

【DevOps实战系列】第三章:详解Maven仓库及环境搭建

个人亲自录制全套DevOps系列实战教程 &#xff1a;手把手教你玩转DevOps全栈技术 Maven私有仓库&#xff0c;就不多说了&#xff0c;我们这里选用最新的Nexus3的3.17版本&#xff0c;平时公司使用的都是Nexus 2.x,新的3.x版本做了很多的升级&#xff0c;包括存储方式等&#xf…

self.eval_net.forward(state)和self.eval_net.forward(state)区别

在根据状态获取一个动作&#xff1a;self.eval_net.forward(state) 在更新网络时&#xff1a;self.eval_net(state) 这2个有什么区别呀&#xff0c;为啥不都是forward 我打印了一下返回值的时候&#xff0c;我感觉格式是一样的 action_value tensor([[0.7177, 0.7369, 0.7124,…