用 JavaScript 編寫中大型程序是離不開 lodash
工具的,而用 TypeScript 編程一樣離不開工具類型的幫助,工具類型就是類型版的 lodash
。簡單的來講,就是把已有的類型通過類型轉換構造一個新的類型。工具類型自己也是類型,得益於泛型的幫助,使其可以對類型進行抽象的處理。工具類型主要目的是簡化類型編程的過程,提升生產力。html
先來看看一個場景,體會下工具類型帶來什麼好處。git
// 一個用戶接口 interface User { name: string avatar: string country:string friend:{ name: string sex: string } }
如今業務要求 User
接口裏的成員都變爲可選,你會怎麼作?再定義一個接口,爲成員都加上可選修飾符嗎?這種方法確實可行,但接口裏有幾十個成員呢?此時,工具類型就能夠派上用場。typescript
type Partial<T> = {[K in keyof T]?: T[K]} type PartialUser = Partial<User> // 此時PartialUser等同於 type PartialUser = { name?: string | undefined; avatar?: string | undefined; country?: string | undefined; friend?: { name: string; sex: string; } | undefined; }
經過工具類型的處理,咱們獲得一個新的類型。即便成員有成千上百個,咱們也只須要一行代碼。因爲 friend
成員是對象,上面的 Partial
處理只對第一層添加可選修飾符,假如須要將對象成員內的成員也添加可選修飾符,可使用 Partial
遞歸來解決。編程
type partial<T> = { [K in keyof T]?: T[K] extends object ? partial<T[K]> : T[K] }
若是你是第一次看到以上的寫法,可能會很懵逼,不知道發生了什麼操做。不慌,且往下看,或許當你看完這篇文章再回過頭來看時,會發現原來是這麼一回事。數組
TypeScript 中的一些關鍵字對於編寫工具類型必不可缺架構
語法: keyof T 。返回聯合類型,爲 T
的全部 key
app
interface User{ name: string age: number } type Man = { name:string, height: 180 } type ManKeys = keyof Man // "name" | "height" type UserKeys = keyof User // "name" | "age"
語法: typeof T 。返回 T
的成員的類型函數
let arr = ['apple', 'banana', 100] let man = { name: 'Jeo', age: 20, height: 180 } type Arr = typeof arr // (string | number)[] type Man = typeof man // {name: string; age: number; height: number;}
相比上面兩個關鍵字, infer
的使用可能會有點難理解。在有條件類型的 extends
子語句中,容許出現 infer
聲明,它會引入一個待推斷的類型變量。這個推斷的類型變量能夠在有條件類型的 true
分支中被引用。工具
簡單來講,它能夠把類型處理過程的某個部分抽離出來當作類型變量。如下例子須要結合高級類型,若是不能理解,能夠選擇跳轉這部分,把高級類型看完後再回來。學習
下面代碼會提取函數類型的返回值類型:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
(...args: any[]) => infer R
和 Function
類型的做用是差很少的,這樣寫只是爲了可以在過程當中拿到函數的返回值類型。 infer
在這裏至關於把返回值類型聲明成一個類型變量,提供給後面的過程使用。
有條件類型能夠嵌套來構成一系列的匹配模式,按順序進行求值:
type Unpacked<T> = T extends (infer U)[] ? U : T extends (...args: any[]) => infer U ? U : T extends Promise<infer U> ? U : T; type T0 = Unpacked<string>; // string type T1 = Unpacked<string[]>; // string type T2 = Unpacked<() => string>; // string type T3 = Unpacked<Promise<string>>; // string type T4 = Unpacked<Promise<string>[]>; // Promise<string> type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string
語法: A & B ,交叉類型能夠把多個類型合併成一個新類型,新類型將擁有全部類型的成員。
interface Shape { size: string color: string } interface Brand { name: string price: number } let clothes: Shape&Brand = { name: 'Uniqlo', color: 'blue', size: 'XL', price: 200 }
語法: typeA | typeB ,聯合類型是包含多種類型的類型,被綁定聯合類型的成員只需知足其中一種類型。
function pushItem(item:string|number){ let array:Array<string|number> = ['apple','banana','cherry'] array.push(item) } pushItem(10) // ok pushItem('durian') // ok
一般,刪除用戶信息須要提供 id
,建立用戶則不須要 id
。這種類型應該如何定義?若是選擇爲 id
字段提供添加可選修飾符的話,那就太不明智了。由於在刪除用戶時,即便不填寫 id
屬性也不會報錯,這不是咱們想要的結果。
可辨識聯合類型能幫助咱們解決這個問題:
type UserAction = { action: 'create' }|{ id:number action: 'delete' } let userAction:UserAction = { id: 1, action: 'delete' }
字⾯量類型主要分爲 真值字⾯量類型,數字字⾯量類型,枚舉字⾯量類型,⼤整數字⾯量類型、字符串字⾯量類型。
const a: 2333 = 2333 // ok const b: 0b10 = 2 // ok const c: 0x514 = 0x514 // ok const d: 'apple' = 'apple' // ok const e: true = true // ok const f: 'apple' = 'banana' // 不能將類型「"banana"」分配給類型「"apple"」
下面以字符串字面量類型做爲例子:
字符串字面量類型容許指定的字符串做爲類型。若是使用 JavaScript 的模式中看下面的例子,會把 level
當成一個值。但在 TypeScript 中,千萬不要用這種思惟去看待, level
表示的就是一個字符串 coder
的類型,被綁定這個類型的變量,它的值只能是 coder
。
type Level = 'coder' let level:Level = 'coder' // ok let level2:Level = 'programmer' // 不能將類型「"programmer"」分配給類型「"coder"」
字符串和聯合類型搭配,能夠實現相似枚舉類型的字符串
type Level = 'coder' | 'leader' | 'boss' function getWork(level: Level){ if(level === 'coder'){ console.log('打代碼、摸魚') }else if(level === 'leader'){ console.log('造輪子、架構') }else if(level === 'boss'){ console.log('喝茶、談生意') } } getWork('coder') getWork('user') // 類型「"user"」的參數不能賦給類型「Level」的參數
語法: T[K] ,使用索引類型,編譯器就可以檢查使用動態屬性名的代碼。在 JavaScript 中,對象能夠用屬性名獲取值,而在 TypeScript 中,這一切被抽象化,變成經過索引獲取類型。就像 person[name]
被抽象成類型 Person[name]
,在如下例子中表明的就是 string
類型。
interface Person { name: string; age: number; } let person: Person = { name: 'Jeo', age: 20 } let name = person['name'] // 'Jeo' type str = Person['name'] // string
咱們能夠在普通的上下文裏使用 T[K]
,只要確保類型變量 K
爲 T
的索引便可
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] { return o[name]; // o[name] is of type T[K] }
getProperty
裏的 o: T
和 name: K
,意味着 o[name]: T[K]
let name: string = getProperty(person, 'name'); let age: number = getProperty(person, 'age'); let unknown = getProperty(person, 'unknown'); // 類型「"unknown"」的參數不能賦給類型「"name" | "age"」的參數
K
不只能夠傳成員,成員的字符串聯合類型也是有效的
type Union = Person[keyof Person] // "string" | "number"
語法: [K in Keys] 。TypeScript 提供了從舊類型中建立新類型的一種方式 。在映射類型裏,新類型以相同的形式去轉換舊類型裏每一個屬性。根據 Keys
來建立類型, Keys
有效值爲 string | number | symbol 或 聯合類型。
type Keys = 'name'|10 type User = { [K in Keys]: string }
該語法能夠理解爲內部使用了循環
所以以上的例子等同於:
type User = { name: string; 10: string; }
須要注意的是這個語法描述的是類型而非成員。若想添加額外的成員,需使用交叉類型:
// 這樣使用 type ReadonlyWithNewMember<T> = { readonly [P in keyof T]: T[P]; } & { newMember: boolean } // 不要這樣使用 // 這會報錯! type ReadonlyWithNewMember<T> = { readonly [P in keyof T]: T[P]; newMember: boolean; }
在真正應用中,映射類型結合索引訪問類型是一個很好的搭配。由於轉換過程會基於一些已存在的類型,且按照必定的方式轉換字段。你能夠把這過程理解爲 JavaScript 中數組的 map
方法,在本來的基礎上擴展元素( TypeScript 中指類型),固然這種理解過程可能有點粗糙。
文章開頭的 Partial
工具類型正是使用這種搭配,爲原有的類型添加可選修飾符。
語法: T extends U ? X : Y ,若 T
可以賦值給 U
,那麼類型是 X
,不然爲 Y
。條件類型以條件表達式推斷類型關係,選擇其中一個分支。相對上面的類型,條件類型很好理解,相似 JavaScript 中的三目運算符。
再來看看文章開頭遞歸的操做,你就會發現能看懂這段處理過程。過程:使用映射類型遍歷,判斷 T[K]
屬於 object
類型,則把 T[K]
傳入 partial
遞歸,不然返回類型 T[K]
。
type partial<T> = { [K in keyof T]?: T[K] extends object ? partial<T[K]> : T[K] }
關於一些經常使用的高級類型相信你們都瞭解得差很少,下面將應用這些類型來編寫一個工具類型。
該工具類型實現的功能爲篩選出兩個 interface
的公共成員:
interface PersonA{ name: string age: number boyfriend: string car: { type: 'Benz' } } interface PersonB{ name: string age: string girlfriend: string car: { type: 'bicycle' } } type Filter<T,U> = T extends U ? T : never type Common<A, B> = { [K in Filter<keyof A, keyof B>]: A[K] extends B[K] ? A[K] : A[K]|B[K] }
經過 Filter
篩選出公共的成員聯合類型 "name"|"age"
做爲映射類型的集合,公共部分可能會存在類型不一樣的狀況,所以要爲成員保留二者的類型。
type CommonMember = Common<PersonA, PersonB> // 等同於 type CommonMember = { name: string; age: string | number; car: { type: "Benz"; } | { type: "bicycle"; }; }
爲了知足常見的類型轉換需求, TypeScript 也提供一些內置工具類型,這些類型是全局可見的。
構造類型 T
,並將它全部的屬性設置爲可選的。它的返回類型表示輸入類型的全部子類型。
interface Todo { title: string; description: string; } function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) { return { ...todo, ...fieldsToUpdate }; } const todo1 = { title: 'organize desk', description: 'clear clutter', }; const todo2 = updateTodo(todo1, { description: 'throw out trash', });
構造類型T,並將它全部的屬性設置爲readonly,也就是說構造出的類型的屬性不能被再次賦值。
interface Todo { title: string; } const todo: Readonly<Todo> = { title: 'Delete inactive users', }; todo.title = 'Hello'; // Error: cannot reassign a readonly property
構造一個類型,其屬性名的類型爲K,屬性值的類型爲T。這個工具可用來將某個類型的屬性映射到另外一個類型上。
interface PageInfo { title: string; } type Page = 'home' | 'about' | 'contact'; const x: Record<Page, PageInfo> = { about: { title: 'about' }, contact: { title: 'contact' }, home: { title: 'home' }, };
從類型T中挑選部分屬性K來構造類型。
interface Todo { title: string; description: string; completed: boolean; } type TodoPreview = Pick<Todo, 'title' | 'completed'>; const todo: TodoPreview = { title: 'Clean room', completed: false, };
從類型T中剔除部分屬性K來構造類型,與Pick相反。
interface Todo { title: string; description: string; completed: boolean; } type TodoPreview = Omit<Todo, 'title' | 'completed'>; const todo: TodoPreview = { description: 'I am description' };
從類型T中剔除全部能夠賦值給U的屬性,而後構造一個類型。
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c" type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c" type T2 = Exclude<string | number | (() => void), Function>; // string | number
從類型T中提取全部能夠賦值給U的類型,而後構造一個類型。
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a" type T1 = Extract<string | number | (() => void), Function>; // () => void
從類型T中剔除null和undefined,而後構造一個類型。
type T0 = NonNullable<string | number | undefined>; // string | number type T1 = NonNullable<string[] | null | undefined>; // string[]
由函數類型T的返回值類型構造一個類型。
type T0 = ReturnType<() => string>; // string type T1 = ReturnType<(s: string) => void>; // void type T2 = ReturnType<(<T>() => T)>; // {} type T3 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[] type T5 = ReturnType<any>; // any type T6 = ReturnType<never>; // any type T7 = ReturnType<string>; // Error type T8 = ReturnType<Function>; // Error
由構造函數類型T的實例類型構造一個類型。
class C { x = 0; y = 0; } type T0 = InstanceType<typeof C>; // C type T1 = InstanceType<any>; // any type T2 = InstanceType<never>; // any type T3 = InstanceType<string>; // Error type T4 = InstanceType<Function>; // Error let t0:T0 = { x: 10, y: 2 }
構造一個類型,使類型T的全部屬性爲required。
interface Props { a?: number; b?: string; }; const obj: Props = { a: 5 }; // OK const obj2: Required<Props> = { a: 5 }; // Error: property 'b' missing
除了介紹編寫工具類型所須要具有的一些知識點,以及 TypeScript 內置的工具類型。更重要的是抽象思惟能力,不難發現上面的例子大部分沒有具體的值運算,都是使用類型在編程。想要理解這些知識,必需要進入到抽象邏輯裏思考。還有高級類型的搭配和類型轉換的處理,也要經過大量的實踐才能玩好。說實話,本身學習這些知識時,真正感覺到 TypeScript 的深不可測,也瞭解到自身的不足之處。忽然想起在某篇文章的一句話:技術是無止盡的,接觸的越多,越能感到本身的眇小。