對於有 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
泛型是強類型語言中比較重要的一個概念,合理的使用泛型能夠提高代碼的可複用性,讓系統更加靈活。下面是維基百科對泛型的描述:數組
泛型容許程序員在強類型程序設計語言中編寫代碼時使用一些之後才指定的類型,在實例化時做爲參數指明這些類型。markdown
泛型經過一對尖括號來表示(<>
),尖括號內的字符被稱爲類型變量,這個變量用來表示類型。函數
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
複製代碼
假如,我如今有兩個類,一個按鈕,一個超連接,如今我須要一個帶有超連接的按鈕,就可使用交叉類型來實現。
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: '按鈕'
}
複製代碼
前面提到的交叉類型與聯合類型若是有多個地方須要使用,就須要經過類型別名的方式,給這兩種類型聲明一個別名。類型別名與聲明變量的語法相似,只須要把 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
相似於 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
關鍵詞不一樣於在 class 後使用 extends
的繼承做用,泛型內使用的主要做用是對泛型加以約束。咱們用咱們前面寫過的 copy 方法再舉個例子:
type BaseType = string | number | boolean
// 這裏表示 copy 的參數
// 只能是字符串、數字、布爾這幾種基礎類型
function copy<T extends BaseType>(arg: T): T {
return arg
}
複製代碼
若是咱們傳入一個對象就會有問題。
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
關鍵詞的做用主要是作類型的映射,遍歷已有接口的 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;
}
複製代碼
條件類型的語法規則和三元表達式一致,常常用於一些類型不肯定的狀況。
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 中內置了不少工具泛型,前面介紹過 Readonly
、Extract
這兩種,內置的泛型在 TypeScript 內置的 lib.es5.d.ts
中都有定義,因此不須要任何依賴都是能夠直接使用的。下面看看一些常用的工具泛型吧。
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>
)
}
複製代碼
type Required<T> = {
[P in keyof T]-?: T[P]
}
複製代碼
Required
的做用恰好與 Partial
相反,就是將接口中全部可選的屬性改成必須的,區別就是把 Partial
裏面的 ?
替換成了 -?
。
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
})
複製代碼
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
}
複製代碼
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'
複製代碼
取出的是 Worker 在 Student 中不存在的 salary
。
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。