為 Vue3 學點 TypeScript, 解讀高階型別

鐵皮飯盒發表於2019-07-30

直達

第一課, 體驗typescript

第二課, 基礎型別和入門高階型別

第三課, 泛型

第四課, 解讀高階型別

第五課, 名稱空間(namespace)是什麼

特別篇, 在vue3?原始碼中學會typescript? - "is"

第六課, 什麼是宣告檔案(declare)? ? - 全域性宣告篇

回顧

第二課的時候為了更好的講解基礎型別, 所以我們講解了一部分高階型別, 比如"介面( interface )" / "聯合型別( | )" / "交叉型別( & )", 本節課我會把剩餘高階型別都講完.

知識點摘要

本節課主要關鍵詞為: 自動型別推斷 / 型別斷言 / 型別別名(type) / 對映型別(Pick/Record等...) / 條件型別(extends) / 型別推斷(infer)

自動型別推斷(不用你標型別了,ts自己猜)

第二課我們講了那麼多基礎型別, 大家現在寫ts的時候一定會在每個變數後面都加上型別吧? 但是?

現在告訴大家有些情況下你不需要標註型別, ts可以根據你寫的程式碼來自動推斷出型別:

賦值字面量給變數

let n = 1; // ts會自動推斷出n是number型別
n+=3 // 不報錯,因為已知型別

let arr1 = []; // 型別為: any[]
arr1.push(1,'2',{a:3}); ,

let arr = [1]; // 內部要有數字, 才能推斷出正確型別
arr.push(2);

複製程式碼
自動閱讀if條件判斷
let n: number|null = 0.5 < Math.random() ? 1:null;
if(null !== n){
    n+=3 // ts知道現在n不是null是number
}
複製程式碼
瀏覽器自帶api
document.ontouchstart = ev=>{ // 能自動推斷出ev為TouchEvent
    console.log(ev.touches);  // 不報錯, TouchEvent上有touches屬性
}
複製程式碼
typeof

typeof就是js中的typeof, ts會根據你程式碼中出現的typeof來自動推斷型別:

let n:number|string = 0.5 < Math.random()? 1:'1';

// 如果沒有typeof, n*=2會報錯, 提示沒法推斷出當前是number型別, 不能進行乘法運算
if('number' === typeof n) {
    n*= 2;
} else  {
    n= '2';
}
複製程式碼

注意: 在ts文件中, 該部分的知識點叫做typeof型別保護, 和其他型別推斷的內容是分開的, 被寫在高階型別/型別保護章節中.

instanceof

ts會根據你程式碼中出現的instanceof來自動推斷型別:

let obj = 0.5 < Math.random() ? new String(1) : new Array(1);
if(obj instanceof String){
    // obj推斷為String型別
    obj+= '123'
} else {
    // obj為any[]型別
    obj.push(123);
}
複製程式碼

注意: 在ts文件中, 該部分的知識點叫做instanceof型別保護, 和其他型別推斷的內容是分開的, 被寫在高階型別/型別保護章節中.

型別斷言(你告訴ts是什麼型別, 他都信)

有些情況下系統沒辦法自動推斷出正確的型別, 就需要我們標記下, 斷言有2種語法, 一種是通過"<>", 一種通過"as", 舉例說明:

let obj = 0.5 < Math.random() ? 1 : [1]; // number|number[]

// 斷言, 告訴ts, obj為陣列
(<number[]>obj).push(1);

//等價
(obj as number[]).push(1);
複製程式碼

型別別名(type)

型別別名可以表示很多介面表示不了的型別, 比如字面量型別(常用來校驗取值範圍):

type A = 'top'|'right'|'bottom'|'left'; // 表示值可能是其中的任意一個
type B = 1|2|3;
type C = '紅'|'綠'|'黃';
type D = 150;

let a:A = 'none'; // 錯誤, A型別中沒有'none'
複製程式碼
更多組合:
interface A1{
    a:number;
}
type B = A1 | {b:string};
type C = A1 & {b:string};

// 與泛型組合
type D<T> = A1 | T[];
複製程式碼

索引型別(keyof)

js中的Object.keys大家肯定都用過, 獲取物件的鍵值, ts中的keyof和他類似, 可以用來獲取物件型別的鍵值:

type A = keyof {a:1,b:'123'} // 'a'|'b'
type B = keyof [1,2] // '0'|'1'|'push'... , 獲取到內容的同時, 還得到了Array原型上的方法和屬性(實戰中暫時沒遇到這種需求, 瞭解即可)
複製程式碼

可以獲得鍵值, 也可以獲取物件型別的值的型別:

type A = {a:1,b:'123'};
type C = A['a'] // 等於type C = 1;
let c:C = 2 // 錯誤, 值只能是1
複製程式碼

對映型別(Readonly, Pick, Record等...)

對映型別比較像修改型別的工具函式, 比如Readonly可以把每個屬性都變成只讀:

type A  = {a:number, b:string}
type A1 = Readonly<A> // {readonly a: number;readonly b: string;}
複製程式碼

開啟node_modules/typescript/lib資料夾可以找到lib.es5.d.ts, 在這我們能找到Readonly的定義:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
複製程式碼

其實不是很複雜, 看了本節課前面前面的內容, 這個很好理解是吧:

  1. 定義一個支援泛型的型別別名, 傳入型別引數T.
  2. 通過keyof獲取T上的鍵值集合.
  3. in表示迴圈keyof獲取的鍵值.
  4. 新增readonly標記.
Partial<T>, 讓屬性都變成可選的
type A  = {a:number, b:string}
type A1 = Partial<A> // { a?: number; b?: string;}
複製程式碼
Required<T>, 讓屬性都變成必選
type A  = {a?:number, b?:string}
type A1 = Required<A> // { a: number; b: string;}
複製程式碼
Pick<T,K>, 只保留自己選擇的屬性, K代表要保留的屬性鍵值
type A  = Pick<{a:number,b:string,c:boolean}, 'a'|'b'>
type A1 = Pick<A, 'a'|'b'> //  {a:number,b:string}
複製程式碼
Omit<T,K> 實現排除已選的屬性
type A  = {a:number, b:string}
type A1 = Omit<A, 'a'> // {b:string}
複製程式碼
Record<K,T>, 建立一個型別,K代表鍵值的型別, T代表值的型別
type A1 = Record<string, string> // 等價{[k:string]:string}
複製程式碼
Exclude<T,U>, 過濾T中和U相同(或相容)的型別
type A  = {a:number, b:string}
type A1 = Exclude<number|string, string|number[]> // number

// 相容
type A2 = Exclude<number|string, any|number[]> // never , 因為any相容number, 所以number被過濾掉
複製程式碼
Extract<T,U>, 提取T中和U相同(或相容)的型別
type A  = {a:number, b:string}
type A1 = Extract<number|string, string|number[]> // string
複製程式碼
NonNullable, 剔除T中的undefined和null
type A1 = NonNullable<number|string|null|undefined> // number|string
複製程式碼
ReturnType, 獲取T的返回值的型別
type A1= ReturnType<()=>number> // number
複製程式碼
InstanceType, 返回T的例項型別

ts中類有2種型別, 靜態部分的型別和例項的型別, 所以T如果是建構函式型別, 那麼InstanceType可以返回他的例項型別:

interface A{
    a:HTMLElement;
}

interface AConstructor{
    new():A;
}

function create (AClass:AConstructor):InstanceType<AConstructor>{
    return new AClass();
}
複製程式碼
Parameters 獲取函式引數型別

返回型別為元祖, 元素順序同引數順序.

interface A{
    (a:number, b:string):string[];
}

type A1 = Parameters<A> // [number, string]
複製程式碼
ConstructorParameters 獲取建構函式的引數型別

Parameters類似, 只是T這裡是建構函式型別.

interface AConstructor{
    new(a:number):string[];
}

type A1 = ConstructorParameters<AConstructor> // [number]
複製程式碼

extends(條件型別)

T extends U ? X : Y
複製程式碼

用來表示型別是不確定的, 如果U的型別可以表示T, 那麼返回X, 否則Y. 舉幾個例子:

type A =  string extends '123' ? string :'123' // '123'
type B =  '123' extends string ? string :123 // string
複製程式碼

明顯string的範圍更大, '123'可以被string表示, 反之不可.

infer(型別推斷)

單詞本身的意思是"推斷", 實際表示在extends條件語句中宣告待推斷的型別變數. 我們上面介紹的對映型別中就有很多都是ts在lib.d.ts中實現的, 比如Parameters:

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
複製程式碼

上面宣告一個P用來表示...args可能的型別, 如果(...args: infer P)可以表示 T, 那麼返回...args對應的型別, 也就是函式的引數型別, 反之返回never.

注意: 開始的T extends (...args: any) => any用來校驗輸入的T是否是函式, 如果不是ts會報錯, 如果直接替換成T不會有報錯, 會一直返回never.

應用infer

接下來我們利用infer來實現"刪除元祖型別中第一個元素", 這常用於簡化函式引數, 這有一個我之前的應用

export type Tail<Tuple extends any[]> = ((...args: Tuple) => void) extends ((a: any, ...args: infer T) => void) ? T : never;
複製程式碼

總結

多寫多練, 很快就上手, 放幾個我用ts寫的專案當做參考, 拋磚引玉, 加油!

手勢庫: github.com/any86/any-t…

命令式呼叫vue元件: github.com/any86/vue-c…

工作中常用的一些程式碼片段: github.com/any86/usefu…

一個mini的事件管理器: github.com/any86/any-e…

微信群

感謝大家的閱讀, 如有疑問可以加群?, 群裡有好多有趣的前端的小夥伴, 讓我們共同學習成長吧!

可加我微信, 我拉你進入微信群(由於騰訊對微信群的100人限制, 超過100人後必須由我拉進去)

為 Vue3 學點 TypeScript, 解讀高階型別

相關文章