你不知道的 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 相关文章推荐
jQuery EasyUI API 中文文档 - Documentation 文档
Sep 29 Javascript
非常有用的40款jQuery 插件推荐(系列二)
Dec 25 Javascript
深入理解JavaScript系列(26):设计模式之构造函数模式详解
Mar 03 Javascript
基于javascript实现浏览器滚动条快到底部时自动加载数据
Nov 30 Javascript
jquery分页插件jquery.pagination.js使用方法解析
Apr 01 Javascript
轻松掌握JavaScript策略模式
Aug 25 Javascript
微信小程序上滑加载下拉刷新(onscrollLower)分批加载数据(一)
May 11 Javascript
node使用Mongoose类库实现简单的增删改查
Nov 08 Javascript
layer.alert自定义关闭回调事件的方法
Sep 27 Javascript
JavaScript如何处理移动端拍摄图片旋转问题
Nov 16 Javascript
Vue element-ui父组件控制子组件的表单校验操作
Jul 17 Javascript
OpenLayer3自定义测量控件MeasureTool
Sep 28 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基础陷阱题(变量赋值)
2012/09/12 PHP
php远程下载类分享
2016/04/13 PHP
PHP 500报错的快速解决方法
2016/12/14 PHP
详解Yii2.0 rules验证规则集合
2017/03/21 PHP
FCK调用方法..
2006/12/21 Javascript
js动态移动滚动条至底部示例代码
2014/04/24 Javascript
js实现使用鼠标拖拽切换图片的方法
2015/05/04 Javascript
教你一步步用jQyery实现轮播器
2016/12/18 Javascript
JavaScript中的 attribute 和 jQuery中的 attr 方法浅析
2017/01/04 Javascript
JS操作xml对象转换为Json对象示例
2017/03/25 Javascript
ES6扩展运算符用法实例分析
2017/10/31 Javascript
bootstrap动态调用select下拉框的实例代码
2018/08/09 Javascript
ES6使用export和import实现模块化的方法
2018/09/10 Javascript
如何在JavaScript中谨慎使用代码注释
2019/06/21 Javascript
Layui弹出层 加载 做编辑页面的方法
2019/09/16 Javascript
vue.js封装switch开关组件的操作
2020/10/26 Javascript
H5 js点击按钮复制文本到粘贴板
2020/11/19 Javascript
python3 与python2 异常处理的区别与联系
2016/06/19 Python
Python标准库使用OrderedDict类的实例讲解
2019/02/14 Python
python实现门限回归方式
2020/02/29 Python
Win10下用Anaconda安装TensorFlow(图文教程)
2020/06/18 Python
Python根据字典的值查询出对应的键的方法
2020/09/30 Python
CSS3结构性伪类选择器九种写法
2012/04/18 HTML / CSS
CSS3中使用RGBa来调节透明度的教程
2016/05/09 HTML / CSS
Foot Locker加拿大官网:美国知名运动产品零售商
2019/07/21 全球购物
摩飞电器俄罗斯官方网站:Morphy Richards俄罗斯
2020/07/30 全球购物
路由表示做什么用的?在linux环境中怎么来配置一条默认路由?
2013/06/07 面试题
护士求职信
2014/07/05 职场文书
勿忘国耻9.18演讲稿(经典篇)
2014/09/14 职场文书
思想作风整顿个人剖析材料
2014/10/06 职场文书
高三毕业评语
2014/12/31 职场文书
时尚女魔头观后感
2015/06/04 职场文书
关于空气污染危害的感想
2015/08/11 职场文书
六五普法学习心得体会
2016/01/21 职场文书
微信小程序 WeUI扩展组件库的入门教程
2022/04/21 Javascript
Redis全局ID生成器的实现
2022/06/05 Redis