关于前端:你不知道的-TypeScript-高级类型

43次阅读

共计 5930 个字符,预计需要花费 15 分钟才能阅读完成。

前言

对于有 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')

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

高级类型

除了 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}

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

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')

这里的 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>

咱们能够构造下这个逻辑,首先 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'

工具泛型

TypesScript 中内置了很多工具泛型,后面介绍过 ReadonlyExtract 这两种,内置的泛型在 TypeScript 内置的 lib.es5.d.ts 中都有定义,所以不须要任何依赖都是能够间接应用的。上面看看一些常常应用的工具泛型吧。

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 是个什么货色。

大抵始终就是类型 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
}

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'

取出的是 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 公布了 4.0 的版本新增了更多功能,想要用好它只能一直的学习和把握它。心愿浏览本文的敌人都能有所播种,解脱 AnyScript。

正文完
 0