1. 程式人生 > 程式設計 >你不知道的 TypeScript 高階型別(小結)

你不知道的 TypeScript 高階型別(小結)

前言

對於有 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 高階型別內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!