你不知道的 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 相关文章推荐
ext combox 下拉框不出现自动提示,自动选中的解决方法
Feb 24 Javascript
Javascript常考语句107条收集
Mar 09 Javascript
JQuery 文本框使用小结
May 22 Javascript
当前流行的JavaScript代码风格指南
Sep 10 Javascript
JavaScript观察者模式(经典)
Dec 09 Javascript
Bootstrap学习笔记之css样式设计(1)
Jun 07 Javascript
动态JavaScript所造成一些你不知道的危害
Sep 25 Javascript
JS中闭包的经典用法小结(2则示例)
Dec 28 Javascript
highcharts 在angular中的使用示例代码
Sep 20 Javascript
关于Google发布的JavaScript代码规范你要知道哪些
Apr 04 Javascript
微信小程序前端自定义分享的实现方法
Jun 13 Javascript
Javascript类型判断相关例题及解析
Aug 26 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
E路文章系统PHP
2006/12/11 PHP
thinkPHP引入类的方法详解
2016/12/08 PHP
PHP文字转图片功能原理与实现方法分析
2017/08/31 PHP
Laravel多用户认证系统示例详解
2018/03/13 PHP
PHP的new static和new self的区别与使用
2019/11/27 PHP
PhpStorm的使用教程(本地运行PHP+远程开发+快捷键)
2020/03/26 PHP
PHP Ajax跨域问题解决方案代码实例
2020/08/01 PHP
JS实现打开本地文件或文件夹
2021/03/09 Javascript
[原创]站长必须要知道的javascript广告代码
2007/05/30 Javascript
js 父窗口控制子窗口的行为-打开,关闭,重定位,回复
2010/04/20 Javascript
Node.js实战 建立简单的Web服务器
2012/03/08 Javascript
用jquery统计子菜单的条数示例代码
2013/10/18 Javascript
js实现简单的星级选择器提交效果适用于评论等
2013/10/18 Javascript
通过js简单实现将一个文本内容转译成加密文本
2013/10/22 Javascript
关于javaScript注册click事件传递参数的不成功问题
2014/07/18 Javascript
js封装tab标签页实例分享
2016/12/19 Javascript
JavaScript之json_动力节点Java学院整理
2017/06/29 Javascript
使用vue-route 的 beforeEach 实现导航守卫(路由跳转前验证登录)功能
2018/03/22 Javascript
JS实现返回上一页并刷新页面的方法分析
2019/07/16 Javascript
JQuery实现ul中添加LI和删除指定的Li元素功能完整示例
2019/10/16 jQuery
浅析Git版本控制器使用
2017/12/10 Python
一份python入门应该看的学习资料
2018/04/11 Python
python3 判断列表是一个空列表的方法
2018/05/04 Python
python如何实现从视频中提取每秒图片
2020/10/22 Python
详解python播放音频的三种方法
2019/09/23 Python
python实现简单银行管理系统
2019/10/25 Python
python实现在线翻译
2020/06/18 Python
Python基于network模块制作电影人物关系图
2020/06/19 Python
python3将变量输入的简单实例
2020/08/19 Python
HTML5表格_动力节点Java学院整理
2017/07/11 HTML / CSS
Mansur Gavriel官网:纽约市的一个设计品牌
2019/05/02 全球购物
老师推荐信
2013/10/28 职场文书
出租车拒载检讨书
2015/01/28 职场文书
毕业生捐书活动倡议书
2015/04/27 职场文书
活动总结模板大全
2015/05/11 职场文书
关于CentOS 8 搭建MongoDB4.4分片集群的问题
2021/10/24 MongoDB