typescript 叹号_TypeScript系列(五)最佳实践
前言
在进入主题之前,我们先来简单回顾一下前四篇文章想要表达的主题:
- 当Redux遇到TypeScript:这篇文章从redux的action出发,介绍了as和可判别联合类型(Discriminated Unions)的使用技巧。
- 从immutable到const contexts:这篇文章主要从一个非常常用的场景出发,介绍了3.4中
as const
的使用技巧。 - 从编程语言到Conditional Types:这篇文章主要从编程语言的视角出发,来看TypeScript中Conditional Type的里程碑式的意义。
- 范型:这篇文章中主要总结了范型的方方面面。
工欲善其事,必先利其器。倘若说,写TS代码是这里的事,之前的文章是利其器,今天的这篇则是如何善。不同人对善有不同的理解,拿整理举个例子,近藤麻理惠认为扔东西很重要,而我妈认为,收整齐存放起来很重要,谁对谁错很难评判。回到TS中的最佳实践,需要根据不同的情况,选择合适的实践,无法一概而论(生搬硬套)。
不能忽略的options
在聊具体代码之前,我们先简短介绍一下这些默认关闭着的、却又非常影响类型系统的实际效果的选项。下面这些选项,若你有类型洁癖,可以都开启。若你想得到类型推导带来的大多数好处,建议至少开启noImplicitAny
和strictNullChecks
。若你只是想获得编辑器的提示、又不想陷入类型的泥淖中,可以保留默认值。
- noImplicitAny 当存在不明确的any时报错。
- noImplicitThis 当this无法正确推导时报错。
- alwaysStrict 在解析的时候使用严格模式,并且为每个文件增加
"use strict"
。 - strictBindCallApply 对使用bind、call和apply调用的函数进行严格的检查。
- strictNullChecks 严格检查可空类型(对于可以根据上下文逻辑,确定非空的地方,可使用感叹号
!
来去除类void的类型)。 - strictFunctionTypes 严格检查函数的参数(主要针对参数继承的情况,用的比较少)。
- strictPropertyInitialization 严格检查非空的类的属性是否都在constructor中进行初始化。
- strict 开启noImplicitAny、noImplicitThis、alwaysStrict、strictBindCallApply、strictNullChecks、strictFunctionTypes和strictPropertyInitialization。
- noImplicitReturns 当存在不明确的return语句时报错。
- noUnusedLocals 当存在没有使用的局部变量时报错。
- noUnusedParameters 当存在没有使用的函数参数时报错(如果需要占位,可使用下划线
_
)。 - suppressImplicitAnyIndexErrors 对于隐式使用索引类型的地方进行检查。
- suppressExcessPropertyErrors 是否检查过量的属性(酌情使用,建议开启)。
副作用(side effect)
前端开发者始终在和副作用做斗争。那到底什么是副作用呢?举一个简单的例子就可以解释清楚了,假如我们写了一段代码,我们本地测试OK后就提交集成测试了,大多数比较正常的情况是,集成失败。一般来说,失败的原因集中在,在某种特殊的条件下,后端接口的返回与预期结果不一致。在这个例子中,API调用属于副作用的一种。通常意义上的副作用是,调用函数后,除了函数的返回值之外,还会对主调用函数产生附加影响。
假如我们将整个页面应用看作一个函数,那么它的副作用一般来源于以下几个方面:
- 网络通信(HTTP调用、WebSockets等)
- 文件或数据IO(localStorage、IndexedDB、sessionStorage、File等)
- 用户交互(按钮点击、表单输入、URL改变等)
这些副作用时时刻刻在影响我们应用的状态,可以说是Bug的源泉。然而,我们却很少善待这一部分的工作。比如,我们通常喜欢快刀斩乱麻似的定义API的结构、然后进入开发环节;我们不假思索的将数据存储在localStorage确很少关心什么时候删除。
遗憾的是,TS不会帮我们解决这些Bug,但它给我们提供了一个解决这些问题的思路,那就是在有Side Effect的地方定义更为明确的类型,比如:
// 一般做法
const getDataFromStorage = (key: string) => {const str = window.localStorage.getItem(key);return str ? JSON.parse(str) : null;
};
const someData = getDataFromStorage("DATA_KEY");
// 推荐做法
function getDataFromStorage<T>(key: string): T | null {const str = window.localStorage.getItem(key);return str ? JSON.parse(str) : null;
}
const someData = getDataFromStorage<DataType>("DATA_KEY");
这里只举了localStorage的情况,对于其它副作用,也是同样的。我们可以基于该例子推广到一般处理副作用的函数上,对于这类函数,最好能够满足这几点:
- 明确返回值的可能性,比如对于
getDataFromStorage
的返回值可能是null、也可能是T类型。 - 提供可定制的范型参数,比如对于
getDataFromStorage
函数而言,根据key的参数不同,返回值的类型可能不同,这里我们可以用范型提供这种定制的可能性。 - 提供明确的范型类型,比如对于
getDataFromStorage
函数而言,这里明确了类型是DataType
而不是由TS默认推导出的{}
类型(在未来的TS版本中,它会被改为unknow
类型)。
如果能够做到这几点,我无法保证项目中的Bug数量会降低一个数量级,但至少会让不清楚API历史的新人踩更少的坑,从而提高团队的效率。
第三方的类型(3rd library types)
任何软件项目都离不开三方库,TS也是如此。TS所提供的第三方的类型定义不止扮演着类型推导中的输入,而且还能为我们提供文档,着实有用的让我们竖起大拇指。现在,NPM上的库可以说是海量了,并且种类版本繁多,很难保证你想要的库有类型定义,有类型定义的类型是正确的,有被合理定义的类型能够被正确使用。
在这个充满未知变量的过程中,如何才能正确地使用TypeScript中的第三方库呢?答案是,因地制宜。这里列举了四种常见的、类型无法按预期工作的情景:
- 官方提供的类型定义与我们的使用情况大相径庭。比如,Immutable.js。
- 官方提供的类型定义不够安全,比如loadash、ramda系列。
- 官方提供的类型定义不够完善,比如很多第三方库。
- 官方提供的类型定义是错的,很少遇到。
对于这些情况,一般有几种解决办法:
- 推动解决官方类型定义的问题,需要较好的英语、耐心的沟通和充裕的时间。(建议)
- 在d.ts文件中完善类型声明,需要深入理解TypeScript,比如这篇文章--TypeScript 在复杂 Immutable.js 数据结构下的用法总结。(建议)
- 鸵鸟法,需要忍受类型的丢失或不可靠性。(退而求其次)
- 懒人法,// @ts-ignore (不推荐)
虽然这些坑比较讨厌,但不得不面对。相比这些外部依赖,下面提几点我们能够掌握的最佳实践。
范型优于联合类型(union type)
联合类型可以说是在TS中最容易被滥用的类型之一。我认为,造成这个现状的锅有至少一半需要官方文档来背,另一半在于我们对范型的不熟悉。
我们先拿举官方文档上的一段示例代码(自己稍微改了下,原始代码是伪代码):
interface Bird {fly(): void;layEggs(): boolean;
}
interface Fish {swim(): void;layEggs(): boolean;
}
// 获得小宠物,这里认为不能够下蛋的宠物是小宠物。现实中的逻辑有点牵强,只是举个例子。
function getSmallPet(...animals: Array<Fish | Bird>): Fish | Bird {for (const animal of animals) {if (!animal.layEggs())return animal;}return animals[0];
}
let pet = getSmallPet();
pet.layEggs(); // okay 因为layEggs是Fish | Bird 共有的方法
pet.swim(); // errors 因为swim是Fish的方法,而这里可能不存在
虽说这个来自TS官网的例子只是为了展示联合类型的机制,但这个例子也误导了很多开发者,并且,它种下了对共有属性(方法)的访问修改(调用)推荐使用联合类型的恶果。为什么说是恶果呢?它主要存在三个问题:
- 类型定义使
getSmallPet
变得局限。从代码逻辑看,它的作用是返回一个不下蛋的动物,返回的类型指向的是Fish或Bird。但我如果只想在一群鸟中挑出一个不下蛋的鸟呢?通过调用这个方法,我只能得到一个或者是Fish、或者是Bird的神奇生物。 - 代码重复、难以扩展。比如,我想再增加一个乌龟。我必须找到所有类似
Fish | Bird
的地方,然后把它修改为Fish | Bird | Turtle
。 - 类型签名无法提供逻辑相关性。我们再审视一下类型签名,完全无法看出这里为什么是
Fish | Bird
而不是其它动物,它们两个到底和逻辑有什么关系才能够被放在这里。
我们可以使用范型重构一下上面的代码,来解决这些问题:
// 将共有的layEggs抽象到Eggable接口
interface Eggable {layEggs(): boolean;
}
interface Bird extends Eggable {fly(): void;
}
interface Fish extends Eggable {swim(): void;
}
// 使用范型,并用范型约束声明接收的动物必须是实现了Eggable接口的类型,也展示了这个函数的作用范围是Eggable
function getSmallPet<T extends Eggable>(...animals: Array<T>): T {for (const animal of animals) {if (!animal.layEggs())return animal;}return animals[0];
}
// 可以用于继承了Eggable的任意类型,比如这里想找到鱼里面不能下蛋的鱼也是可以的
let pet = getSmallPet<Fish>();
pet.layEggs(); // okay
pet.swim(); // okay
到这里可能有读者要问了,你举的这个例子简直是个套路,是故意设计出来的,我们代码中很多使用联合类型的地方没这么简单的能用范型重构。我的回答是,例子的确是套路,但它是个典型情况,比较能说明问题。在实际项目中,它容易被滥用在很多地方,比如React的高阶组件中、比如处理相似逻辑的函数中。有兴趣的读者可以搜一下实际项目中使用联合类型的地方,看看有多少地方能够应用这样的重构。
巧用typeof优于自定义类型
在副作用一节,强调了为副作用定义良好类型的重要性。这一节恰好相反,是为了强调在没有副作用的代码中,使用typeof
偷懒的重要性。在前端页面,大多数数据来源于服务器,但有少部分数据是写死在前端的。对于这些写死的数据,除本系列第二篇文章提到一种处理手段外,我们有时候还需要这样的类型参与到计算中,这时,可以使用typeof
来获取我们想要的类型。
还是举个例子吧。比如,我们定义一个URL列表,并且要为类型是通知的标题加上[通知]两个子,代码如下:
enum URLTypes {NORMAL,NOTIFICATION,
}
const urlConfigs = [{type: URLTypes.NORMAL,title: "普通页面",component: "div",},{type: URLTypes.NOTIFICATION,title: "通知页面",component: "p",},
];
// 提取UrlConfigItem的类型,以便给getTitle函数使用
type UrlConfigItem = typeof urlConfigs[0];
function getTitle(item: UrlConfigItem, title: string) {if (item.type === URLTypes.NOTIFICATION) {return `[通知]-${title}`;}return title;
}
这样,我们可以借助于TS,少些一些类型定义。另一个通常能够用到这个技巧的地方是类的函数,有时,我们希望将一个函数的返回值传给另一个函数,而这时我们又不想对函数接收的参数进行重复的类型定义,这种情况下,我们可以使用ReturnType获得函数的返回类型,然后用于另一个函数的参数。
最后
上面提到的最佳实践是比较重要的一些。限于篇幅,一些零碎的实践这里就不说了,这些按下不表的实践也不是作者比较吝啬,而是很难用一个例子来说明情况,比如合理的拷贝优于过度地使用extends、API层级的类型转换优于使用时转换等等。若以后有机会或想到典型的例子,再和大家分享吧。
顺带一提,收藏的同时记得点赞哦,有用的知识属于周围每一位奋斗者。
typescript 叹号_TypeScript系列(五)最佳实践相关推荐
- RxJava系列7(最佳实践)
RxJava系列1(简介) RxJava系列2(基本概念及使用介绍) RxJava系列3(转换操作符) RxJava系列4(过滤操作符) RxJava系列5(组合操作符) RxJava系列6(从微观角 ...
- OssImport系列之四——最佳实践
相关文章:OssImport系列之一--架构 本文主要介绍,OssImport在典型场景下应用,典型需求的实现. 单机与分布式 OssImport有 单机模式 和 分布式模式 两种部署方式.对于小于 ...
- VR系列——Oculus最佳实践:二、双眼视觉,立体成像和深度线索
大脑利用眼睛的不同视角来感知物体的深度. 不要忽略单眼的深度线索,比如说纹理和亮度. 用户使用虚拟现实头戴显示器的最佳深度范围在0.75米至3.5米之间(在Unity里,1单位=1米) 虚拟摄像头之间 ...
- VR系列——Oculus最佳实践:九、用户输入和导航
对于VR而言,没有任何传统输入方法是完美的,但游戏手柄在当前还是最好的选择:变革和调研是必要的(Oculus也在积极探索中). 用户从Rift中不能看到他们的输入设备:要让他们使用熟悉的控制器,以便能 ...
- VR系列——Oculus最佳实践:七、虚拟幻境头晕(下)
失真校正 Rift的镜头扭曲的显示屏上显示的图形,这是由SDK给出的后期处理步骤校正的.这种扭曲被正确的完成,并根据SDK的准则和所提供的实例演示是极为重要的.不正确的失真可以"看" ...
- 【华为云会议开发指南】最佳实践
开放性应用实践概览 华为云会议提供了服务端API开放和客户端SDK开放,开发者基于自己的应用场景可以灵活地集成华为云会议的开放性接口.本文介绍了几个基于华为云会议集成的最佳实践,帮助开发者了解几种典型 ...
- 最佳实践系列丨Docker EE 服务发现参考架构(二)
出品丨Docker公司(ID:docker-cn) 编译丨小东 每周一.三.五晚6点10分 与您不见不散 服务发现对服务进行注册并发布其连接信息,以使其他服务了解如何连接到服务.随着应用向微服务和面向 ...
- 天云大数据_【案例分享】天云大数据最佳实践系列之——信用评分模型
本文为天云大数据原创 大数据能力特有的性质,使其正在成为大型银行真正的核心竞争力.银行大数据能力表现在多方面,但大数据思维和数据挖掘能力是最关键.也是最重要的.天云大数据自成立以来,一直深耕于金融领域 ...
- 信创办公–基于WPS的EXCEL最佳实践系列 (筛选重要数据)
信创办公–基于WPS的EXCEL最佳实践系列 (筛选重要数据) 目录 应用背景 操作步骤 1.筛选 2.高级筛选 应用背景 在WPS里,筛选有两种,一种是筛选,另外一种则是高级筛选. 操作步骤 1.筛 ...
最新文章
- [CSS]复选框单选框与文字对齐问题的研究与解决.
- Robo 3T SQL
- java 使用gzip压缩和解压 传输文件必备
- guava_学习_00_资源帖
- C++优先队列自定义排序总结
- 最适合晚上睡不着看的 8 个网站,建议收藏哦
- 读书笔记 - 简约之美:软件设计之道
- Spotfire 设置 组合图表的刻度范围
- 英语----形容词和副词
- STM32——红外接收和红外发射
- 已知华氏温度f c语言,编程题:已知两种温度的换算公式C=(5/9)(F-32),试编写一个程序输入华氏度F,输出摄氏度。...
- 使用ABAP批量下载有道云笔记中的图片
- 基于Linux的及时通信软件
- SCU 4438 Censo (字符串hash)
- 版权问题某些资源无法下载
- rabbitmq链接超时_RabbitMQ前置SLB中TCP连接超时900秒限制
- Python类中的__dict__属性
- 毕业设计 stm32智能电子秤系统 - 物联网 嵌入式 单片机
- win7 删除java_windows7系统卸载java的操作方法?
- 拉起微信三方登录,详细实现步骤
热门文章
- Error in match.names(clabs, names(xi)) : names do not match previous names
- R语言使用gt包和gtExtras包优雅地、漂亮地显示表格数据:使用gt包可视化表格数据,使其易于阅读和理解、使用gtExtras包添加一个图,显示表中某一列中的数字
- R语言plotly可视化:plotly可视化基本散点图(指定图像类型、模式)、plotly可视化散点图(为不同分组数据配置不同的色彩)、ggplotly使用plotly包呈现ggplot2的可视化结果
- pandas对dataframe的数据行进行随机抽样(Random Sample of Rows):使用sample函数进行数据行随机抽样(有放回的随机抽样,replacement)
- R语言cut函数实现数据分箱及因子化实战
- R语言case_when函数和cases函数实战
- 通过tushare获取贵州茅台和中国平安历史交易数据并使用plotly进行可视化分析
- 服务器上安装运行fastqc
- 基因组序列及注释数据下载
- 第二章 序列比对——Blast局部比对