你不知道的 TypeScript 高级类型(小结)


Posted in Javascript onAugust 28, 2020

前言

对于有 JavaScript 基础的同学来说,入门 TypeScript 其实很容易,只需要简单掌握其基础的类型系统就可以逐步将 JS 应用过渡到 TS 应用。

// js
const double = (num) => 2 * num

// ts
const double = (num: number): number => 2 * num

然而,当应用越来越复杂,我们很容易把一些变量设置为 any 类型,TypeScript 写着写着也就成了 AnyScript。为了让大家能更加深入的了解 TypeScript 的类型系统,本文将重点介绍其高级类型,帮助大家摆脱 AnyScript。

泛型

在讲解高级类型之前,我们需要先简单理解泛型是什么。

泛型是强类型语言中比较重要的一个概念,合理的使用泛型可以提升代码的可复用性,让系统更加灵活。下面是维基百科对泛型的描述:

泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。

泛型通过一对尖括号来表示( <> ),尖括号内的字符被称为 类型变量 ,这个变量用来表示类型。

function copy<T>(arg: T): T {
 if (typeof arg === 'object') {
 return JSON.parse(
  JSON.stringify(arg)
 )
 } else {
 return arg
 }
}

这个类型 T,在没有调用 copy 函数的时候并不确定,只有调用 copy 的时候,我们才知道 T 具体代表什么类型。

const str = copy<string>('my name is typescript')

你不知道的 TypeScript 高级类型(小结)

我们在 VS Code 中可以看到 copy 函数的参数以及返回值已经有了类型,也就是说我们调用 copy 函数的时候,给类型变量 T 赋值了 string。其实,我们在调用 copy 的时候可以省略尖括号,通过 TS 的类型推导是可以确定 T 为 string 的。

你不知道的 TypeScript 高级类型(小结)

高级类型

除了 string、number、boolean 这种基础类型外,我们还应该了解一些类型声明中的一些高级用法。

交叉类型(&)

交叉类型说简单点就是将多个类型合并成一个类型,个人感觉叫做「合并类型」更合理一点,其语法规则和逻辑 “与” 的符号一致。

T & U

假如,我现在有两个类,一个按钮,一个超链接,现在我需要一个带有超链接的按钮,就可以使用交叉类型来实现。

interface Button {
 type: string
 text: string
}

interface Link {
 alt: string
 href: string
}

const linkBtn: Button & Link = {
 type: 'danger',
 text: '跳转到百度',
 alt: '跳转到百度',
 href: 'http://www.baidu.com'
}

联合类型(|)

联合类型的语法规则和逻辑 “或” 的符号一致,表示其类型为连接的多个类型中的任意一个。

T | U

例如,之前的 Button 组件,我们的 type 属性只能指定固定的几种字符串。

interface Button {
 type: 'default' | 'primary' | 'danger'
 text: string
}

const btn: Button = {
 type: 'primary',
 text: '按钮'
}

类型别名(type)

前面提到的交叉类型与联合类型如果有多个地方需要使用,就需要通过类型别名的方式,给这两种类型声明一个别名。类型别名与声明变量的语法类似,只需要把 constlet 换成 type 关键字即可。

type Alias = T | U

type InnerType = 'default' | 'primary' | 'danger'

interface Button {
 type: InnerType
 text: string
}

interface Alert {
 type: ButtonType
 text: string
}

类型索引(keyof)

keyof 类似于 Object.keys ,用于获取一个接口中 Key 的联合类型。

interface Button {
 type: string
 text: string
}

type ButtonKeys = keyof Button
// 等效于
type ButtonKeys = "type" | "text"

还是拿之前的 Button 类来举例,Button 的 type 类型来自于另一个类 ButtonTypes,按照之前的写法,每次 ButtonTypes 更新都需要修改 Button 类,如果我们使用 keyof 就不会有这个烦恼。

interface ButtonStyle {
 color: string
 background: string
}
interface ButtonTypes {
 default: ButtonStyle
 primary: ButtonStyle
 danger: ButtonStyle
}
interface Button {
 type: 'default' | 'primary' | 'danger'
 text: string
}

// 使用 keyof 后,ButtonTypes修改后,type 类型会自动修改 
interface Button {
 type: keyof ButtonTypes
 text: string
}

类型约束(extends)

这里的 extends 关键词不同于在 class 后使用 extends 的继承作用,泛型内使用的主要作用是对泛型加以约束。我们用我们前面写过的 copy 方法再举个例子:

type BaseType = string | number | boolean

// 这里表示 copy 的参数
// 只能是字符串、数字、布尔这几种基础类型
function copy<T extends BaseType>(arg: T): T {
 return arg
}

你不知道的 TypeScript 高级类型(小结)

如果我们传入一个对象就会有问题。

你不知道的 TypeScript 高级类型(小结)

extends 经常与 keyof 一起使用,例如我们有一个方法专门用来获取对象的值,但是这个对象并不确定,我们就可以使用 extendskeyof 进行约束。

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

const obj = { a: 1 }
const a = getValue(obj, 'a')

你不知道的 TypeScript 高级类型(小结)

这里的 getValue 方法就能根据传入的参数 obj 来约束 key 的值。

类型映射(in)

in 关键词的作用主要是做类型的映射,遍历已有接口的 key 或者是遍历联合类型。下面使用内置的泛型接口 Readonly 来举例。

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

interface Obj {
 a: string
 b: string
}

type ReadOnlyObj = Readonly<Obj>

你不知道的 TypeScript 高级类型(小结)

我们可以结构下这个逻辑,首先 keyof Obj 得到一个联合类型 'a' | 'b'

interface Obj {
 a: string
 b: string
}

type ObjKeys = 'a' | 'b'

type ReadOnlyObj = {
 readonly [P in ObjKeys]: Obj[P];
}

然后 P in ObjKeys 相当于执行了一次 forEach 的逻辑,遍历 'a' | 'b'

type ReadOnlyObj = {
 readonly a: Obj['a'];
 readonly b: Obj['b'];
}

最后就可以得到一个新的接口。

interface ReadOnlyObj {
 readonly a: string;
 readonly b: string;
}

条件类型(U ? X : Y)

条件类型的语法规则和三元表达式一致,经常用于一些类型不确定的情况。

T extends U ? X : Y

上面的意思就是,如果 T 是 U 的子集,就是类型 X,否则为类型 Y。下面使用内置的泛型接口 Extract 来举例。

type Extract<T, U> = T extends U ? T : never;

如果 T 中的类型在 U 存在,则返回,否则抛弃。假设我们两个类,有三个公共的属性,可以通过 Extract 提取这三个公共属性。

interface Worker {
 name: string
 age: number
 email: string
 salary: number
}

interface Student {
 name: string
 age: number
 email: string
 grade: number
}


type CommonKeys = Extract<keyof Worker, keyof Student>
// 'name' | 'age' | 'email'

你不知道的 TypeScript 高级类型(小结)

工具泛型

TypesScript 中内置了很多工具泛型,前面介绍过 ReadonlyExtract 这两种,内置的泛型在 TypeScript 内置的 lib.es5.d.ts 中都有定义,所以不需要任何依赖都是可以直接使用的。下面看看一些经常使用的工具泛型吧。

你不知道的 TypeScript 高级类型(小结)

Partial

type Partial<T> = {
 [P in keyof T]?: T[P]
}

Partial 用于将一个接口的所有属性设置为可选状态,首先通过 keyof T ,取出类型变量 T 的所有属性,然后通过 in 进行遍历,最后在属性后加上一个 ?

我们通过 TypeScript 写 React 的组件的时候,如果组件的属性都有默认值的存在,我们就可以通过 Partial 将属性值都变成可选值。

import React from 'react'

interface ButtonProps {
 type: 'button' | 'submit' | 'reset'
 text: string
 disabled: boolean
 onClick: () => void
}

// 将按钮组件的 props 的属性都改为可选
const render = (props: Partial<ButtonProps> = {}) => {
 const baseProps = {
 disabled: false,
 type: 'button',
 text: 'Hello World',
 onClick: () => {},
 }
 const options = { ...baseProps, ...props }
 return (
 <button
  type={options.type}
  disabled={options.disabled}
  onClick={options.onClick}>
  {options.text}
 </button>
 )
}

Required

type Required<T> = {
 [P in keyof T]-?: T[P]
}

Required 的作用刚好与 Partial 相反,就是将接口中所有可选的属性改为必须的,区别就是把 Partial 里面的 ? 替换成了 -?

Record

type Record<K extends keyof any, T> = {
 [P in K]: T
}

Record 接受两个类型变量, Record 生成的类型具有类型 K 中存在的属性,值为类型 T。这里有一个比较疑惑的点就是给类型 K 加一个类型约束, extends keyof any ,我们可以先看看 keyof any 是个什么东西。

你不知道的 TypeScript 高级类型(小结)

大致一直就是类型 K 被约束在 string | number | symbol 中,刚好就是对象的索引的类型,也就是类型 K 只能指定为这几种类型。

我们在业务代码中经常会构造某个对象的数组,但是数组不方便索引,所以我们有时候会把对象的某个字段拿出来作为索引,然后构造一个新的对象。假设有个商品列表的数组,要在商品列表中找到商品名为 「每日坚果」的商品,我们一般通过遍历数组的方式来查找,比较繁琐,为了方便,我们就会把这个数组改写成对象。

interface Goods {
 id: string
 name: string
 price: string
 image: string
}

const goodsMap: Record<string, Goods> = {}
const goodsList: Goods[] = await fetch('server.com/goods/list')

goodsList.forEach(goods => {
 goodsMap[goods.name] = goods
})

Pick

type Pick<T, K extends keyof T> = {
 [P in K]: T[P]
}

Pick 主要用于提取接口的某几个属性。做过 Todo 工具的同学都知道,Todo工具只有编辑的时候才会填写描述信息,预览的时候只有标题和完成状态,所以我们可以通过 Pick 工具,提取 Todo 接口的两个属性,生成一个新的类型 TodoPreview。

interface Todo {
 title: string
 completed: boolean
 description: string
}

type TodoPreview = Pick<Todo, "title" | "completed">

const todo: TodoPreview = {
 title: 'Clean room',
 completed: false
}

你不知道的 TypeScript 高级类型(小结)

Exclude

type Exclude<T, U> = T extends U ? never : T

Exclude 的作用与之前介绍过的 Extract 刚好相反,如果 T 中的类型在 U 不存在,则返回,否则抛弃。现在我们拿之前的两个类举例,看看 Exclude 的返回结果。

interface Worker {
 name: string
 age: number
 email: string
 salary: number
}

interface Student {
 name: string
 age: number
 email: string
 grade: number
}


type ExcludeKeys = Exclude<keyof Worker, keyof Student>
// 'name' | 'age' | 'email'

你不知道的 TypeScript 高级类型(小结)

取出的是 Worker 在 Student 中不存在的 salary

Omit

type Omit<T, K extends keyof any> = Pick<
 T, Exclude<keyof T, K>
>

Omit 的作用刚好和 Pick 相反,先通过 Exclude<keyof T, K> 先取出类型 T 中存在,但是 K 不存在的属性,然后再由这些属性构造一个新的类型。还是通过前面的 Todo 案例来说,TodoPreview 类型只需要排除接口的 description 属性即可,写法上比之前 Pick 精简了一些。

interface Todo {
 title: string
 completed: boolean
 description: string
}

type TodoPreview = Omit<Todo, "description">

const todo: TodoPreview = {
 title: 'Clean room',
 completed: false
}

你不知道的 TypeScript 高级类型(小结)

总结

如果只是掌握了 TypeScript 的一些基础类型,可能很难游刃有余的去使用 TypeScript,而且最近 TypeScript 发布了 4.0 的版本新增了更多功能,想要用好它只能不断的学习和掌握它。希望阅读本文的朋友都能有所收获,摆脱 AnyScript。

到此这篇关于你不知道的 TypeScript 高级类型(小结)的文章就介绍到这了,更多相关TypeScript 高级类型内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
alixixi runcode.asp的代码不错的应用
Aug 08 Javascript
来自国外的页面JavaScript文件优化
Dec 08 Javascript
js 利用className得到对象的实现代码
Nov 15 Javascript
JavaScript面向对象设计二 构造函数模式
Dec 20 Javascript
JavaScript实现拼音排序的方法
Nov 20 Javascript
JQuery中根据属性或属性值获得元素(6种情况获取方法)
Jan 17 Javascript
javascript改变position值实现菜单滚动至顶部后固定
Jan 18 Javascript
关于json字符串与实体之间的严格验证代码
Nov 10 Javascript
微信小程序 登录的简单实现
Apr 19 Javascript
underscore之Chaining_动力节点Java学院整理
Jul 10 Javascript
深入浅析AngularJs模版与v-bind
Jul 06 Javascript
vue在手机中通过本机IP地址访问webApp的方法
Aug 15 Javascript
js和jquery判断数据类型的4种方法总结
Aug 28 #jQuery
Node在Controller层进行数据校验的过程详解
Aug 28 #Javascript
Postman无法正常返回结果问题解决
Aug 28 #Javascript
Vue+Element UI 树形控件整合下拉功能菜单(tree + dropdown +input)
Aug 28 #Javascript
vue自定义指令和动态路由实现权限控制
Aug 28 #Javascript
vue 动态给每个页面添加title、关键词和描述的方法
Aug 28 #Javascript
vue-cli+webpack项目打包到服务器后,ttf字体找不到的解决操作
Aug 28 #Javascript
You might like
php 中include()与require()的对比
2006/10/09 PHP
php addslashes及其他清除空格的方法是不安全的
2012/01/25 PHP
Laravel访问出错提示:`Warning: require(/vendor/autoload.php): failed to open stream: No such file or di解决方法
2019/04/02 PHP
javascript 函数调用的对象和方法
2010/07/01 Javascript
使用jquery实现图文切换效果另加特效
2013/01/20 Javascript
点击按钮或链接不跳转只刷新页面的脚本整理
2013/10/22 Javascript
jQuery中选择器小问题(新人难免遇到)
2014/03/31 Javascript
jQuery实现ctrl+enter(回车)提交表单
2015/10/19 Javascript
JavaScript实现自动生成网页元素功能(按钮、文本等)
2015/11/21 Javascript
理解javascript中DOM事件
2015/12/25 Javascript
如何快速上手Vuex
2017/02/14 Javascript
gulp加批处理(.bat)实现ng多应用一键自动化构建
2017/02/16 Javascript
Angularjs根据json文件动态生成路由状态的实现方法
2017/04/17 Javascript
Vue 解决父组件跳转子路由后当前导航active样式消失问题
2020/07/21 Javascript
Python urllib、urllib2、httplib抓取网页代码实例
2015/05/09 Python
Python单例模式的两种实现方法
2017/08/14 Python
mac下给python3安装requests库和scrapy库的实例
2018/06/13 Python
python opencv旋转图像(保持图像不被裁减)
2018/07/26 Python
python爬虫URL重试机制的实现方法(python2.7以及python3.5)
2018/12/18 Python
python变量赋值方法(可变与不可变)
2019/01/12 Python
Python进度条的制作代码实例
2019/08/31 Python
Python爬虫解析网页的4种方式实例及原理解析
2019/12/30 Python
QT5 Designer 打不开的问题及解决方法
2020/08/20 Python
用HTML5.0制作网页的教程
2010/05/30 HTML / CSS
HTML5 Canvas旋转动画的2个代码例子(一个旋转的太极图效果)
2014/04/10 HTML / CSS
浅谈HTML5 FileReader分布读取文件以及其方法简介
2017/11/09 HTML / CSS
科茨沃尔德家居商店:Scotts of Stow
2018/06/29 全球购物
IdealFit官方网站:女性蛋白质、补充剂和运动服装
2019/03/24 全球购物
什么叫做SQL注入,如何防止
2016/10/04 面试题
甜品蛋糕店创业计划书范文
2014/02/06 职场文书
党员入党表决心的话
2014/03/11 职场文书
大四学生找工作的自荐信
2014/03/27 职场文书
项目申报专员岗位职责
2014/07/09 职场文书
公司行政专员岗位职责
2014/08/24 职场文书
2016十一国庆节感言
2015/12/09 职场文书
React 高阶组件HOC用法归纳
2021/06/13 Javascript