你不知道的 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')
類型
我們在 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)
前面提到的交叉類型與聯合類型如果有多個地方需要使用,就需要通過類型別名的方式,給這兩種類型聲明一個別名。類型別名與聲明變量的語法類似,只需要把 const、let 換成 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 一起使用,例如我們有一個方法專門用來獲取對象的值,但是這個對象并不確定,我們就可以使用 extends 和 keyof 進行約束。
- 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 中內置了很多工具泛型,前面介紹過 Readonly、Extract 這兩種,內置的泛型在 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>
- // 'name' | 'age' | 'email'
ExcludeKeys
取出的是 Worker 在 Student 中不存在的 salary。
Omit
- type Omit<T, K extends keyof any> = Pick<
- T, Exclude<keyof T, K>
- >
Omit 的作用剛好和 Pick 相反,先通過 Exclude
- 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。
本文轉載自微信公眾號「更了不起的前端」,可以通過以下二維碼關注。轉載本文請聯系更了不起的前端公眾號。