本節將對TypeScript中類型的高級特性做詳細講解,包括交叉類型、類型別名、類型保護等。
## 一、交叉類型
  交叉類型(Intersection Type)是將多個類型通過“&”符號合并成一個新類型,新類型將包含所有類型的特性。例如有Person和Programmer兩個類(如下代碼所示),當man變量的類型聲明為Person&Programmer時,它就能使用兩個類的成員:name屬性和work()方法。
~~~
class Person {
name: string;
}
class Programmer {
work() { }
}
let man: Person&Programmer;
man.name;
man.work();
~~~
  交叉類型常用于混入(mixin)或其它不適合典型面向對象模型的場景,例如在下面的示例中,通過交叉類型讓新對象obj同時包含a和b兩個屬性。
~~~
function extend<T, U>(first: T, second: U): T & U {
const result = <T & U>{};
for (let prop in first) {
(<T>result)[prop] = first[prop];
}
for (let prop in second) {
if (!result.hasOwnProperty(prop)) {
(<U>result)[prop] = second[prop];
}
}
return result;
}
let obj = extend({ a: 1 }, { b: 2 });
~~~
## 二、類型別名
  TypeScript提供了type關鍵字,用于創建類型別名,可作用于基本類型、聯合類型、交叉類型和泛型等任意類型,如下所示。
~~~
type Name = string; //基本類型
type Func = () => string; //函數
type Union = Name | Func; //聯合類型
type Tuple = [number, number]; //元組
type Generic<T> = { value: T }; //泛型
~~~
  注意,起別名不是新建一個類型,而是提供一個可讀性更高的名稱。類型別名可在屬性里引用自身,但不能出現在聲明的右側,如下所示。
~~~
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
type Arrs = Array<Arrs>; //錯誤
~~~
## 三、類型保護
  當使用聯合類型時,只能訪問它們的公共成員。假設有一個func()函數,它的參數是由Person和Programmer兩個類組成的聯合類型,如下代碼所示。
~~~
function func(man: Person | Programmer) {
if((<Person>man).run) {
(<Person>man).run();
}else {
(<Programmer>man).work();
}
}
~~~
  雖然利用類型斷言可以確定參數類型,在編譯階段避免了報錯,但是多次調用類型斷言未免過于繁瑣。于是TypeScript就引入了類型保護機制,替代類型斷言。類型保護(Type Guard)是一些表達式,允許在運行時檢查類型,縮小類型范圍。
**1)typeof**
  TypeScript可將typeof運算符識別成類型保護,從而就能直接在代碼里檢查類型(如下所示),其計算結果是個字符串,包括“number”、“string”、“boolean”或“symbol”等關鍵字。
~~~
function send(data: number | string) {
if (typeof data === "number") {
//...
} else if(typeof data === "string") {
//...
}
}
~~~
**2)instanceof**
  TypeScript也可將instanceof運算符識別成類型保護,通過構造函數來細化類型,檢測實例和類是否有關聯,如下所示。
~~~
function work(man: Person | Programmer) {
if (man instanceof Person) {
//...
} else if(man instanceof Programmer) {
//...
}
}
~~~
**3)自定義**
  TypeScript還允許自定義類型保護,其形式和函數聲明類似,只是返回類型需要改成類型謂詞,如下所示。
~~~
function isPerson(man: Person | Programmer): man is Person {
return !!(<Person>man).run;
}
~~~
  類型謂詞由當前函數的參數名稱、is關鍵字和指定的類型名稱所組成。
## 四、字面量類型
  TypeScript可將字符串字面量作為一個類型,用于指定一個字符串類型的固定值。當該類型與聯合類型、類型別名等特性配合使用時,可以模擬出枚舉的效果,如下所示。
~~~
type Direction = "Up" | "Down" | "Left";
function move(data: Direction) {
return data;
}
move("Up"); //正確
move("Right"); //錯誤
~~~
  move()函數只能接收Direction類型的三個固定值,傳入其它值都會產生錯誤。
  字符串字面量類型還可以用來區分函數重載,如下所示。
~~~
function run(data: "Left"): string;
function run(data: "Down"): string;
function run(data: string) {
return data;
}
~~~
  其它常見的字面量類型還有數字和布爾值,如下所示。
~~~
type Numbers = 1 | 2 | 3 | 4 | 5 | 6;
type Bools = true | false;
~~~
  注意,字面量類型屬于單例類型。單例類型是一種只有一個值的類型,當每個枚舉成員都用字面量初始化時,枚舉成員是具有類型的,叫枚舉成員類型,它也屬于單例類型。
## 五、可辨析聯合
  通過合并單例類型、聯合類型、類型保護和類型別名可創建一種高級模式:可辨析聯合(Discriminated Union),也叫做標簽聯合或代數數據類型。TypeScript中的可辨析聯合具有3個要素:
  (1)具有單例類型的屬性,即可辨析的特征或標簽。
  (2)一個聯合了多個類型的類型別名。
  (3)針對第一個要素中的屬性的類型保護。
  在下面的示例中,首先聲明了兩個接口,每個接口都有字符串字面量類型的kind屬性,并且其值都不同,而kind屬性就是第一個要素中的可辨析的特征或標簽。
~~~
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
~~~
  然后將兩個接口聯合,并創建一個類型別名,實現第二個要素,如下所示。
~~~
type Shape = Rectangle | Circle;
~~~
  最后通過具有判斷性的kind屬性,結合switch語句,執行類型保護,縮小類型范圍,如下所示。
~~~
function caculate(s: Shape) {
switch (s.kind) {
case "rectangle":
return s.height * s.width;
case "circle":
return Math.PI * s.radius ** 2;
}
}
~~~
**1)完整性檢查**
  當未涵蓋可辨析聯合的所有變化時,需要能反饋到編譯器中。例如新增Square接口,并將它添加到Shape類型中(如下所示),如果未更新caculate()函數,那么就不能編譯通過。
~~~
interface Square {
kind: "square";
size: number;
}
type Shape = Rectangle | Circle | Square;
~~~
  有兩種方法能實現這種預警,第一種是在輸入編譯命令時添加--strictNullChecks參數,并為caculate()函數指定返回值類型,如下所示。
~~~
function caculate(s: Shape): number {
switch (s.kind) {
case "rectangle":
return s.height * s.width;
case "circle":
return Math.PI * s.radius ** 2;
}
}
~~~
  由于switch語句沒有包含所有類型,因此TypeScript會認為該函數有可能返回undefined,從而就會編譯報錯。注意,這種方法不太精確,有很多因素(例如函數默認返回數字)會干擾完整性檢查,并且--strictNullChecks參數對舊代碼有兼容問題。
  第二種方法是使用never類型,如下代碼所示,新增一個能引發類型錯誤的assertNever()函數,并在default分支中調用該函數。
~~~
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
function caculate(s: Shape) {
switch (s.kind) {
case "rectangle":
return s.height * s.width;
case "circle":
return Math.PI * s.radius ** 2;
default:
return assertNever(s);
}
}
~~~
  雖然額外定義了一個函數,但是檢查的精確度提升了不少。
## 六、索引類型
  索引類型(Index Type)能讓編譯器檢查使用動態屬性的場景,例如從對象中選取屬性的子集,如下所示。
~~~
function pluck(obj, names) {
return names.map(n => obj[n]);
}
~~~
  如果要讓pluck()函數能從obj對象中成功的選出names數組所指定的屬性,那么需要在聲明時設置類型約束,包括names中的元素必須是obj中存在的屬性以及返回值類型得是obj屬性值的類型,下面通過泛型來描述這些約束。
~~~
function pluck<T, K extends keyof T>(obj: T, names: K[]): T[K][] {
return names.map(n => obj[n]);
}
interface Person {
name: string;
age: number;
}
let person: Person = {
name: "strick",
age: 28
};
let attrs: string[] = pluck(person, ["name"]);
~~~
  泛型函數pluck()引入了兩個新的類型操作符,分別是索引類型查詢操作符(keyof T)和索引訪問操作符(T\[K\])。前者會取T類型中由公共(public)屬性名所組成的聯合類型,例如“"name" | "age"”;后者會取T類型中指定屬性值的類型,這意味著示例中的person\["name"\]和Person\["name"\]兩者的類型都是string。
**1)字符串索引簽名**
  keyof T與T\[K\]同樣適用于字符串索引簽名,以下面的泛型接口People為例,kType的類型是string和number的聯合類型,因為JavaScript里的數值索引會自動轉換成字符串索引;vType的類型是number,也就是索引簽名的類型。
~~~
interface People<T> {
[key: string]: T;
}
let kType: keyof People<number>; //string | number
let vType: People<number>["name"]; //number
~~~
## 七、映射類型
  映射類型(Mapped Type)與索引類型類似,也是從現有類型中創建出一種新類型。接下來用一個例子來演示映射類型用法,假設有一個Person接口,它有兩個成員,如下所示。
~~~
interface Person {
name: string;
age: number;
}
~~~
  當需要將Person接口的每個成員都變為可選或只讀的,粗糙的解決方法是一個個的修改,如下所示。
~~~
interface PersonPartial {
name?: string;
age?: number;
}
interface PersonReadonly {
readonly name: string;
readonly age: number;
}
~~~
  而如果采用映射類型,那么就能快速的改變接口成員,如下代碼所示,其中Readonly可將T類型的成員設為只讀,而Partial會將它們設為可選。
~~~
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]?: T[P];
}
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
~~~
  \[P in keyof T\]的語法與索引類型的類似,但內部使用了for-in遍歷語句,其中:
  (1)P是類型變量,會依次綁定到每個成員上,對應成員名的類型。
  (2)T是由字符串字面量構成的聯合類型,表示一組成員名,例如“"name" | "age"”。
  (3)T\[P\]是成員值的類型。
  注意,映射類型描述的是類型而非成員,如果要添加額外的成員,需要使用交叉類型的方式,如下所示,直接在類型中添加成員會無法通過編譯。
~~~
//交叉類型
type ReadonlyNew<T> = {
readonly [P in keyof T]: T[P];
} & { data: boolean };
//編譯錯誤
type ReadonlyNew<T> = {
readonly [P in keyof T]: T[P];
data: boolean;
};
~~~
  Readonly和Partial是一種同態轉換,即在映射時保留源類型的成員名以及其值類型,并且與目標類型相比只有修飾符有差異。而那些會創建新成員、改變成員類型或其值類型的轉換都被稱為非同態。由于Readonly和Partial很實用,因此它們已經被包含進TypeScript的標準庫里,作為內置的工具類型存在。
## 八、條件類型
  條件類型(Conditional Type)能夠表示非統一的類型映射,常以條件表達式進行類型檢測,語法類似于三目運算符,從兩個類型中選出一個,如下所示。
~~~
T extends U ? X : Y
~~~
  如果T是U的子類型,那么類型將被解析成X,否則是Y。當條件的真假無法確定時,得到的結果將是由X和Y組成的聯合類型,以下面的全局函數sum()為例,T是布爾值的子類型,當傳入的參數是true時,得到的將是string類型;而傳入false時,得到的是number類型。
~~~
declare function sum<T extends boolean>(x: T): T extends true ? string : number;
let x = sum(true); //string | number
~~~
  如果T或U包含類型變量,那么就得延遲解析,即等到類型變量都有具體類型后才能計算出條件類型的結果。在下面的示例中,創建了一個Person接口,聲明的全局函數add()的返回值類型會根據是否是Person的子類型而改變,并且在泛型函數func()中調用了add()函數。
~~~
interface Person {
name: string;
age: number;
getName(): string;
}
declare function add<T>(x: T): T extends Person ? string : number;
function func<U>(x: U) {
let a = add(x);
let b: string | number = a;
}
~~~
  雖然a變量的類型尚不確定,但是條件類型的結果不是string就是number,因此可以成功的賦給b變量。
**1)分布式條件類型**
  當條件類型中被檢查的類型是無類型參數(naked type parameter)時,它會被稱為分布式條件類型(Distributive Conditional Type)。其特殊之處在于它能自動分布聯合類型,舉個簡單的例子,假設T的類型是A | B | C,那么它會被解析成三個條件分支,如下所示。
~~~
(A | B | C) extends U ? X : Y
//等同于
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
~~~
  分布式條件類型可以用來過濾聯合類型,如下所示,Filter類型可從T中移除U的子類型。
~~~
type Filter<T, U> = T extends U ? never : T;
type T1 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
type T2 = Filter<string | number | (() => void), Function>; // string | number
~~~
  分布式條件類型也可與映射類型配合使用,進行針對性的類型映射,即不同源類型對應不同映射規則,例如映射接口的方法名,如下所示。
~~~
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
type T3 = FunctionPropertyNames<Person>; // "getName"
~~~
  注意,條件類型與聯合類型和交叉類型相似,不允許遞歸地引用自身,下面這樣寫會在編譯階段報錯。
~~~
type Custom<T> = T extends any[] ? Custom<T[number]> : T;
~~~
**2)類型推斷**
  在條件類型的extends子句中,允許通過infer聲明引入一個待推斷的類型變量,并且可出現多個同類型的infer聲明,例如用infer聲明來提取函數的返回值類型,如下所示。有一點要注意,只能在true分支中使用infer聲明的類型變量。
~~~
type Func<T> = T extends (...args: any[]) => infer R ? R : any;
~~~
  當函數具有重載時,就取最后一個函數簽名進行推斷,如下所示,其中ReturnType是內置的條件類型,可獲取函數類型T的返回值類型。
~~~
declare function load(x: string): number;
declare function load(x: number): string;
declare function load(x: string | number): string | number;
type T4 = ReturnType<typeof load>; // string | number
~~~
  注意,無法在正常類型參數的約束子語句中使用infer聲明,如下所示。
~~~
type Func<T extends (...args: any[]) => infer R> = R;
~~~
  但是可以將約束里的類型變量移除,并將其轉移到條件類型中,就能達到相同的效果,如下所示。
~~~
type AnyFunction = (...args: any[]) => any;
type Func<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : any;
~~~
**3)預定義的條件類型**
  除了之前示例中用到的ReturnType之外,TypeScript還預定義了4個其它功能的條件類型,如下所列。
  (1)Exclude:從T中移除掉U的子類型。
  (2)Extract:從T中篩選出U的子類型。
  (3)NonNullable:從T中移除null與undefined。
  (4)InstanceType:獲取構造函數的實例類型。
~~~
type T11 = Exclude<"a" | "b" | "c" | "d", "a" | "c">; // "b" | "d"
type T12 = Extract<"a" | "b" | "c" | "d", "a" | "c">; // "a" | "c"
type T13 = NonNullable<string | number | undefined>; // string | number
type T14 = ReturnType<(s: string) => void>; // void
class Programmer {
name: string;
}
type T15 = InstanceType<typeof Programmer>; //Programmer
~~~
*****
> 原文出處:
[博客園-TypeScript躬行記](https://www.cnblogs.com/strick/category/1561745.html)
[知乎專欄-TypeScript躬行記](https://zhuanlan.zhihu.com/pwts2019)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎瀏覽。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020