TypeScript高級(jí)類(lèi)型:聯(lián)合類(lèi)型和交叉類(lèi)型介紹

TypeScript的基礎(chǔ)類(lèi)型、字面量類(lèi)型、函數(shù)類(lèi)型及接口類(lèi)型,它們都是單一、原子的類(lèi)型元素。其實(shí),有一些稍微復(fù)雜、實(shí)際編程場(chǎng)景,我們還需要通過(guò)組合/結(jié)合單一、原子類(lèi)型構(gòu)造更復(fù)雜的類(lèi)型,以此描述更復(fù)雜的數(shù)據(jù)和結(jié)構(gòu)。這就是使用聯(lián)合和交叉類(lèi)型(Unions and Intersection Types)。

聯(lián)合類(lèi)型

聯(lián)合類(lèi)型(Unions)用來(lái)表示變量、參數(shù)的類(lèi)型不是單一原子類(lèi)型,而可能是多種不同的類(lèi)型的組合。

我們主要通過(guò)“|”操作符分隔類(lèi)型的語(yǔ)法來(lái)表示聯(lián)合類(lèi)型。這里,我們可以把“|”類(lèi)比為 JavaScript 中的邏輯或 “||”,只不過(guò)前者表示可能的類(lèi)型。

舉個(gè)例子,我們封裝了一個(gè)將 string 或者 number 類(lèi)型的輸入值轉(zhuǎn)換成 '數(shù)字 + "px" 格式的函數(shù),如下代碼所示:

function formatPX(size: unknown) {

? if (typeof size === 'number') {

? ? return `${size}px`;

? }

? if (typeof size === 'string') {

? ? return `${parseInt(size) || 0}px`;

? }

? throw Error(` 僅支持 number 或者 string`);

}

formatPX(13);

formatPX('13px');

在學(xué)習(xí)聯(lián)合類(lèi)型之前,我們可能免不了使用 any 或 unknown 類(lèi)型來(lái)表示參數(shù)的類(lèi)型(為了讓大家養(yǎng)成好習(xí)慣,推薦使用 unknown)。

通過(guò)這樣的方式帶來(lái)的問(wèn)題是,在調(diào)用 formatPX 時(shí),我們可以傳遞任意的值,并且可以通過(guò)靜態(tài)類(lèi)型檢測(cè)(使用 any 亦如是),但是運(yùn)行時(shí)還是會(huì)拋出一個(gè)錯(cuò)誤,例如:

formatPX(true);

formatPX(null);

這顯然不符合我們的預(yù)期,因?yàn)?size 應(yīng)該是更明確的,即可能也只可能是 number 或 string 這兩種可選類(lèi)型的類(lèi)型。

所幸有聯(lián)合類(lèi)型,我們可以使用一個(gè)更明確表示可能是 number 或 string 的聯(lián)合類(lèi)型來(lái)注解 size 參數(shù),如下代碼所示:

function formatPX(size: number | string) {

? // ...

}

formatPX(13); // ok

formatPX('13px'); // ok

formatPX(true); // ts(2345) 'true' 類(lèi)型不能賦予 'number | string' 類(lèi)型

formatPX(null); // ts(2345) 'null' 類(lèi)型不能賦予 'number | string' 類(lèi)型

在第 1 行,我們定義了函數(shù) formatPX 的參數(shù) size 既可以是 number 類(lèi)型也可以是 string 類(lèi)型,所以第 5 行和第 6 行傳入數(shù)字 13 和字符串 '13px' 都正確,但在第 8 行和第 9 行傳入布爾類(lèi)型的 true 或者 null 類(lèi)型都會(huì)提示一個(gè) ts(2345) 錯(cuò)誤。

當(dāng)然,我們可以組合任意個(gè)、任意類(lèi)型來(lái)構(gòu)造更滿(mǎn)足我們?cè)V求的類(lèi)型。比如,我們希望給前邊的示例再加一個(gè) unit 參數(shù)表示可能單位,這個(gè)時(shí)候就可以聲明一個(gè)字符串字面類(lèi)型組成的聯(lián)合類(lèi)型,如下代碼所示:

function formatUnit(size: number | string, unit: 'px' | 'em' | 'rem' | '%' = 'px') {

? // ...

}

formatUnit(1, 'em'); // ok

formatUnit('1px', 'rem'); // ok

formatUnit('1px', 'bem'); // ts(2345)

我們定義了 formatPX 函數(shù)的第二個(gè)參數(shù) unit,它的類(lèi)型是由 'px'、'em'、'rem'、'%' 字符串字面類(lèi)型組成的類(lèi)型集合。因此,我們可以在第 5 行和第 6 行傳入字符串字面量 'em' 和 'rem' 作為第二個(gè)實(shí)參。如果在第 8 行我們傳入一個(gè)不在類(lèi)型集合中的字符串字面量 'bem' ,就會(huì)提示一個(gè) ts(2345) 錯(cuò)誤。

我們也可以使用類(lèi)型別名抽離上邊的聯(lián)合類(lèi)型,然后再將其進(jìn)一步地聯(lián)合,如下代碼所示:

type ModernUnit = 'vh' | 'vw';

type Unit = 'px' | 'em' | 'rem';

type MessedUp = ModernUnit | Unit; // 類(lèi)型是 'vh' | 'vw' | 'px' | 'em' | 'rem'

這里我們定義了 ModernUnit 別名表示 'vh' 和 'vw' 這兩個(gè)字面量類(lèi)型的組合,且定義了 Unit 別名表示 'px' 和 'em' 和 'rem' 字面量類(lèi)型組合,同時(shí)又定義了 MessedUp 別名表示 ModernUnit 和 Unit 兩個(gè)類(lèi)型別名的組合。

這里埋一個(gè)伏筆: 如果將 string 原始類(lèi)型和“string 字面量類(lèi)型”組合成一個(gè)聯(lián)合類(lèi)型會(huì)是什么效果?你可以自己嘗試一下,答案將在這一講的最后揭曉。

我們也可以把接口類(lèi)型聯(lián)合起來(lái)表示更復(fù)雜的結(jié)構(gòu),如下所示示例:

interface Bird {

? fly(): void;

? layEggs(): void;

}

interface Fish {

? swim(): void;

? layEggs(): void;

}

const getPet: () => Bird | Fish = () => {

? return {

? // ...

? } as Bird | Fish;

};

const Pet = getPet();

Pet.layEggs(); // ok

Pet.fly(); // ts(2339) 'Fish' 沒(méi)有 'fly' 屬性; 'Bird | Fish' 沒(méi)有 'fly' 屬性

從上邊的示例可以看到,在聯(lián)合類(lèi)型中,我們可以直接訪(fǎng)問(wèn)各個(gè)接口成員都擁有的屬性、方法,且不會(huì)提示類(lèi)型錯(cuò)誤。但是,如果是個(gè)別成員特有的屬性、方法,我們就需要區(qū)分對(duì)待了,此時(shí)又要引入類(lèi)型守衛(wèi)來(lái)區(qū)分不同的成員類(lèi)型。

只不過(guò),在這種情況下,我們還需要使用基于 in 操作符判斷的類(lèi)型守衛(wèi),如下代碼所示:

if (typeof Pet.fly === 'function') { // ts(2339)

? Pet.fly(); // ts(2339)

}

if ('fly' in Pet) {

? Pet.fly(); // ok

}

因?yàn)?Pet 的類(lèi)型既可能是 Bird 也可能是 Fish,這就意味著在第 1 行可能會(huì)通過(guò) Fish 類(lèi)型獲取 fly 屬性,但 Fish 類(lèi)型沒(méi)有 fly 屬性定義,所以會(huì)提示一個(gè) ts(2339) 錯(cuò)誤。

交叉類(lèi)型

前邊我們使用了邏輯或“||” 類(lèi)比聯(lián)合類(lèi)型,那是不是還有一個(gè)邏輯與“&&”可以類(lèi)比類(lèi)型?

在 TypeScript 中,確實(shí)還存在一種類(lèi)似邏輯與行為的類(lèi)型——交叉類(lèi)型(Intersection Type),它可以把多個(gè)類(lèi)型合并成一個(gè)類(lèi)型,合并后的類(lèi)型將擁有所有成員類(lèi)型的特性。

在 TypeScript 中,我們可以使用“&”操作符來(lái)聲明交叉類(lèi)型,如下代碼所示:

{

? type Useless = string & number;

}

很顯然,如果我們僅僅把原始類(lèi)型、字面量類(lèi)型、函數(shù)類(lèi)型等原子類(lèi)型合并成交叉類(lèi)型,是沒(méi)有任何用處的,因?yàn)槿魏晤?lèi)型都不能滿(mǎn)足同時(shí)屬于多種原子類(lèi)型,比如既是 string 類(lèi)型又是 number 類(lèi)型。因此,在上述的代碼中,類(lèi)型別名 Useless 的類(lèi)型就是個(gè) never。

合并接口類(lèi)型

聯(lián)合類(lèi)型真正的用武之地就是將多個(gè)接口類(lèi)型合并成一個(gè)類(lèi)型,從而實(shí)現(xiàn)等同接口繼承的效果,也就是所謂的合并接口類(lèi)型,如下代碼所示:

? type IntersectionType = { id: number; name: string; }

? ? & { age: number };

? const mixed: IntersectionType = {

? ? id: 1,

? ? name: 'name',

? ? age: 18

? }

在上述示例中,我們通過(guò)交叉類(lèi)型,使得 IntersectionType 同時(shí)擁有了 id、name、age 所有屬性,這里我們可以試著將合并接口類(lèi)型理解為求并集。

這里,我們來(lái)發(fā)散思考一下:如果合并的多個(gè)接口類(lèi)型存在同名屬性會(huì)是什么效果呢?

此時(shí),我們可以根據(jù)同名屬性的類(lèi)型是否兼容將這個(gè)問(wèn)題分開(kāi)來(lái)看。

如果同名屬性的類(lèi)型不兼容,比如上面示例中兩個(gè)接口類(lèi)型同名的 name 屬性類(lèi)型一個(gè)是 number,另一個(gè)是 string,合并后,name 屬性的類(lèi)型就是 number 和 string 兩個(gè)原子類(lèi)型的交叉類(lèi)型,即 never,如下代碼所示:

? type IntersectionTypeConfict = { id: number; name: string; }

? ? & { age: number; name: number; };

? const mixedConflict: IntersectionTypeConfict = {

? ? id: 1,

? ? name: 2, // ts(2322) 錯(cuò)誤,'number' 類(lèi)型不能賦給 'never' 類(lèi)型

? ? age: 2

? };

此時(shí),我們賦予 mixedConflict 任意類(lèi)型的 name 屬性值都會(huì)提示類(lèi)型錯(cuò)誤。而如果我們不設(shè)置 name 屬性,又會(huì)提示一個(gè)缺少必選的 name 屬性的錯(cuò)誤。在這種情況下,就意味著上述代碼中交叉出來(lái)的 IntersectionTypeConfict 類(lèi)型是一個(gè)無(wú)用類(lèi)型。

如果同名屬性的類(lèi)型兼容,比如一個(gè)是 number,另一個(gè)是 number 的子類(lèi)型、數(shù)字字面量類(lèi)型,合并后 name 屬性的類(lèi)型就是兩者中的子類(lèi)型。

如下所示示例中 name 屬性的類(lèi)型就是數(shù)字字面量類(lèi)型 2,因此,我們不能把任何非 2 之外的值賦予 name 屬性。

? type IntersectionTypeConfict = { id: number; name: 2; }

? & { age: number; name: number; };

? let mixedConflict: IntersectionTypeConfict = {

? ? id: 1,

? ? name: 2, // ok

? ? age: 2

? };

? mixedConflict = {

? ? id: 1,

? ? name: 22, // '22' 類(lèi)型不能賦給 '2' 類(lèi)型

? ? age: 2

? };

合并聯(lián)合類(lèi)型

另外,我們可以合并聯(lián)合類(lèi)型為一個(gè)交叉類(lèi)型,這個(gè)交叉類(lèi)型需要同時(shí)滿(mǎn)足不同的聯(lián)合類(lèi)型限制,也就是提取了所有聯(lián)合類(lèi)型的相同類(lèi)型成員。這里,我們也可以將合并聯(lián)合類(lèi)型理解為求交集。

在如下示例中,兩個(gè)聯(lián)合類(lèi)型交叉出來(lái)的類(lèi)型 IntersectionUnion 其實(shí)等價(jià)于 'em' | 'rem',所以我們只能把 'em' 或者 'rem' 字符串賦值給 IntersectionUnion 類(lèi)型的變量。

? type UnionA = 'px' | 'em' | 'rem' | '%';

? type UnionB = 'vh' | 'em' | 'rem' | 'pt';

? type IntersectionUnion = UnionA & UnionB;

? const intersectionA: IntersectionUnion = 'em'; // ok

? const intersectionB: IntersectionUnion = 'rem'; // ok

? const intersectionC: IntersectionUnion = 'px'; // ts(2322)

? const intersectionD: IntersectionUnion = 'pt'; // ts(2322)

既然是求交集,如果多個(gè)聯(lián)合類(lèi)型中沒(méi)有相同的類(lèi)型成員,交叉出來(lái)的類(lèi)型自然就是 never 了,如下代碼所示:

? type UnionC = 'em' | 'rem';

? type UnionD = 'px' | 'pt';

? type IntersectionUnionE = UnionC & UnionD;

? const intersectionE: IntersectionUnionE = 'any' as any; // ts(2322) 不能賦予 'never' 類(lèi)型

在上述示例中,因?yàn)?UnionC 和 UnionD 沒(méi)有交集,交叉出來(lái)的類(lèi)型 IntersectionUnionE 就是 never,所以我們不能把任何類(lèi)型的值賦予 IntersectionUnionE 類(lèi)型的變量。

聯(lián)合、交叉組合

在前面的示例中,我們把一些聯(lián)合、交叉類(lèi)型抽離成了類(lèi)型別名,再把它作為原子類(lèi)型進(jìn)行進(jìn)一步的聯(lián)合、交叉。其實(shí),聯(lián)合、交叉類(lèi)型本身就可以直接組合使用,這就涉及 |、& 操作符的優(yōu)先級(jí)問(wèn)題。實(shí)際上,聯(lián)合、交叉運(yùn)算符不僅在行為上表現(xiàn)一致,還在運(yùn)算的優(yōu)先級(jí)和 JavaScript 的邏輯或 ||、邏輯與 && 運(yùn)算符上表現(xiàn)一致 。

聯(lián)合操作符 | 的優(yōu)先級(jí)低于交叉操作符 &,同樣,我們可以通過(guò)使用小括弧 () 來(lái)調(diào)整操作符的優(yōu)先級(jí)。

? type UnionIntersectionA = { id: number; } & { name: string; } | { id: string; } & { name: number; }; // 交叉操作符優(yōu)先級(jí)高于聯(lián)合操作符

? type UnionIntersectionB = ('px' | 'em' | 'rem' | '%') | ('vh' | 'em' | 'rem' | 'pt'); // 調(diào)整優(yōu)先級(jí)

進(jìn)而,我們也可以把分配率、交換律等基本規(guī)則引入類(lèi)型組合中,然后優(yōu)化出更簡(jiǎn)潔、清晰的類(lèi)型,如下代碼所示:

? type UnionIntersectionC = ({ id: number; } & { name: string; } | { id: string; }) & { name: number; };

? type UnionIntersectionD = { id: number; } & { name: string; } & { name: number; } | { id: string; } & { name: number; }; // 滿(mǎn)足分配率

? type UnionIntersectionE = ({ id: string; } | { id: number; } & { name: string; }) & { name: number; }; // 滿(mǎn)足交換律

在上述代碼中,第 2 行是在第 1 行的基礎(chǔ)上進(jìn)行展開(kāi),說(shuō)明 & 滿(mǎn)足分配率;第 3 行則是在第 1 行的基礎(chǔ)上調(diào)整了成員的順序,說(shuō)明 | 操作滿(mǎn)足交換律。

類(lèi)型縮減

這里呼應(yīng)一下在介紹聯(lián)合類(lèi)型時(shí)埋下的伏筆:如果將 string 原始類(lèi)型和“string字面量類(lèi)型”組合成聯(lián)合類(lèi)型會(huì)是什么效果?效果就是類(lèi)型縮減成 string 了。

同樣,對(duì)于 number、boolean(其實(shí)還有枚舉類(lèi)型,詳見(jiàn)第 9 講)也是一樣的縮減邏輯,如下所示示例:

? type URStr = 'string' | string; // 類(lèi)型是 string

? type URNum = 2 | number; // 類(lèi)型是 number

? type URBoolen = true | boolean; // 類(lèi)型是 boolean

? enum EnumUR {

? ? ONE,

? ? TWO

? }

? type URE = EnumUR.ONE | EnumUR; // 類(lèi)型是 EnumUR

TypeScript 對(duì)這樣的場(chǎng)景做了縮減,它把字面量類(lèi)型、枚舉成員類(lèi)型縮減掉,只保留原始類(lèi)型、枚舉類(lèi)型等父類(lèi)型,這是合理的“優(yōu)化”。

可是這個(gè)縮減,卻極大地削弱了 IDE 自動(dòng)提示的能力,如下代碼所示:

? type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string; // 類(lèi)型縮減成 string

在上述代碼中,我們希望 IDE 能自動(dòng)提示顯示注解的字符串字面量,但是因?yàn)轭?lèi)型被縮減成 string,所有的字符串字面量 black、red 等都無(wú)法自動(dòng)提示出來(lái)了。

不要慌,TypeScript 官方其實(shí)還提供了一個(gè)黑魔法,它可以讓類(lèi)型縮減被控制。如下代碼所示,我們只需要給父類(lèi)型添加“& {}”即可。

? type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string & {}; // 字面類(lèi)型都被保留

VS Code 自動(dòng)提示效果圖

此時(shí),其他字面量類(lèi)型就不會(huì)被縮減掉了,在 IDE 中字符串字面量 black、red 等也就自然地可以自動(dòng)提示出來(lái)了。

此外,當(dāng)聯(lián)合類(lèi)型的成員是接口類(lèi)型,如果滿(mǎn)足其中一個(gè)接口的屬性是另外一個(gè)接口屬性的子集,這個(gè)屬性也會(huì)類(lèi)型縮減,如下代碼所示:

? type UnionInterce =

? | {

? ? ? age: '1';

? ? }

? | ({

? ? ? age: '1' | '2';

? ? ? [key: string]: string;

? ? });

這里因?yàn)?'1' 是 '1' | '2' 的子集,所以 age 的屬性變成 '1' | '2':

利用這個(gè)特性,我們來(lái)實(shí)現(xiàn) 07 講中埋下的那個(gè)伏筆,如何定義如下所示 age 屬性是數(shù)字類(lèi)型,而其他不確定的屬性是字符串類(lèi)型的數(shù)據(jù)結(jié)構(gòu)的對(duì)象?

{

? age: 1, // 數(shù)字類(lèi)型

? anyProperty: 'str', // 其他不確定的屬性都是字符串類(lèi)型

? ...

}

在這里提到這個(gè)伏筆,想必你應(yīng)該明白了,我們肯定要用到兩個(gè)接口的聯(lián)合類(lèi)型及類(lèi)型縮減,這個(gè)問(wèn)題的核心在于找到一個(gè)既是 number 的子類(lèi)型,這樣 age 類(lèi)型縮減之后的類(lèi)型就是 number;同時(shí)也是 string 的子類(lèi)型,這樣才能滿(mǎn)足屬性和 string 索引類(lèi)型的約束關(guān)系。

哪個(gè)類(lèi)型滿(mǎn)足這個(gè)條件呢?我們一起回憶一下 02 講中介紹的特殊類(lèi)型 never。

never 有一個(gè)特性是它是所有類(lèi)型的子類(lèi)型,自然也是 number 和 string 的子類(lèi)型,所以答案如下代碼所示:

? type UnionInterce =

? | {

? ? ? age: number;

? ? }

? | ({

? ? ? age: never;

? ? ? [key: string]: string;

? ? });

? const O: UnionInterce = {

? ? age: 2,

? ? string: 'string'

? };

在上述代碼中,我們?cè)诘?3 行定義了 number 類(lèi)型的 age 屬性,第 6 行定義了 never 類(lèi)型的 age 屬性,等價(jià)于 age 屬性的類(lèi)型是由 number 和 never 類(lèi)型組成的聯(lián)合類(lèi)型,所以我們可以把 number 類(lèi)型的值(比如說(shuō)數(shù)字字面量 1)賦予 age 屬性;但是不能把其他任何類(lèi)型的值(比如說(shuō)字符串字面量 'string' )賦予 age。

同時(shí),我們?cè)诘?5 行~第 8 行定義的接口類(lèi)型中,還額外定義了 string 類(lèi)型的字符串索引簽名。因?yàn)?never 同時(shí)又是 string 類(lèi)型的子類(lèi)型,所以 age 屬性的類(lèi)型和字符串索引簽名類(lèi)型不沖突。如第 9 行~第 12 行所示,我們可以把一個(gè) age 屬性是 2、string 屬性是 'string' 的對(duì)象字面量賦值給 UnionInterce 類(lèi)型的變量 O。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容