你不知道的 TypeScript 高級類型

前言

對於有 JavaScript 基礎的同窗來講,入門 TypeScript 其實很容易,只須要簡單掌握其基礎的類型系統就能夠逐步將 JS 應用過渡到 TS 應用。react

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

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

然而,當應用愈來愈複雜,咱們很容易把一些變量設置爲 any 類型,TypeScript 寫着寫着也就成了 AnyScript。爲了讓你們能更加深刻的瞭解 TypeScript 的類型系統,本文將重點介紹其高級類型,幫助你們擺脫 AnyScript。程序員

泛型

在講解高級類型以前,咱們須要先簡單理解泛型是什麼。typescript

泛型是強類型語言中比較重要的一個概念,合理的使用泛型能夠提高代碼的可複用性,讓系統更加靈活。下面是維基百科對泛型的描述:數組

泛型容許程序員在強類型程序設計語言中編寫代碼時使用一些之後才指定的類型,在實例化時做爲參數指明這些類型。

泛型經過一對尖括號來表示(<>),尖括號內的字符被稱爲類型變量,這個變量用來表示類型。函數

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 這種基礎類型外,咱們還應該瞭解一些類型聲明中的一些高級用法。fetch

交叉類型(&)

交叉類型說簡單點就是將多個類型合併成一個類型,我的感受叫作「合併類型」更合理一點,其語法規則和邏輯 「與」 的符號一致。ui

T & U

假如,我如今有兩個類,一個按鈕,一個超連接,如今我須要一個帶有超連接的按鈕,就可使用交叉類型來實現。es5

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
}

copy number

若是咱們傳入一個對象就會有問題。

copy object

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>

ReadOnlyObj

咱們能夠結構下這個邏輯,首先 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'

CommonKeys

工具泛型

TypesScript 中內置了不少工具泛型,前面介紹過 ReadonlyExtract 這兩種,內置的泛型在 TypeScript 內置的 lib.es5.d.ts 中都有定義,因此不須要任何依賴都是能夠直接使用的。下面看看一些常用的工具泛型吧。

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 是個什麼東西。

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
}

TodoPreview

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>
// 'salary'

ExcludeKeys

取出的是 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
}

TodoPreview

總結

若是隻是掌握了 TypeScript 的一些基礎類型,可能很難遊刃有餘的去使用 TypeScript,並且最近 TypeScript 發佈了 4.0 的版本新增了更多功能,想要用好它只能不斷的學習和掌握它。但願閱讀本文的朋友都能有所收穫,擺脫 AnyScript。

image

相關文章
相關標籤/搜索