碼文不易,轉載請帶上本文連結,感謝~ https://www.cnblogs.com/echoyya/p/14558034.html
型別斷言
型別斷言(Type Assertion): 主要用於當 TypeScript 推斷出來型別並不滿足當前需求時,TypeScript 允許開發者覆蓋它的推斷,可以用來手動指定一個值的型別。
型別斷言是一個編譯時語法,不涉及執行時。
語法
值 as 型別
(推薦)或 <型別>值
形如 <Foo>
的語法在 ts 中除了表示型別斷言之外,也可能是表示一個泛型,故建議在使用型別斷言時,使用 值 as 型別
語法。
型別斷言的用途
型別斷言的常見用途有以下幾種:
聯合型別可以被斷言為其中一個型別
上一篇文章中介紹訪問聯合型別的屬性和方法,當 TS 不確定一個聯合型別的變數到底是哪個型別時,只能訪問此聯合型別的所有型別中共有的屬性或方法:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
// 介面可看作一種型別
function getName(animal: Cat | Fish) {
return animal.name;
}
而有時確實需要在還不確定型別的時候就訪問其中一個型別特有的屬性或方法,如:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
if (typeof animal.swim === 'function') { // 報錯
return true;
}
return false;
}
// Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.
上述報錯可使用型別斷言,將 animal
斷言成 Fish
,就可以解決訪問 animal.swim
報錯的問題。
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}
注意:型別斷言只能夠欺騙 TS 編譯器,無法避免執行時的錯誤,反而濫用型別斷言可能會導致執行時錯誤:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function swim(animal: Cat | Fish) {
(animal as Fish).swim();
}
const tom: Cat = {
name: 'Tom',
run() { console.log('run') }
};
swim(tom);
// 編譯時不會報錯,但在執行時會報錯 Uncaught TypeError: animal.swim is not a function`
原因是 (animal as Fish).swim()
這段程式碼將 animal
直接斷言為 Fish
,隱藏了 animal
可能為 Cat
的情況,而 TS 編譯器信任了我們的斷言,故在呼叫 swim()
時沒有編譯錯誤。
可是 swim
函式接受的引數型別是 Cat | Fish
,一旦傳入的引數是 Cat
型別的變數,由於 Cat
上沒有 swim
方法,就會導致執行時錯誤。
總之,使用型別斷言時一定要格外小心,儘量避免斷言後呼叫方法或引用深層屬性,以減少不必要的執行時錯誤。
父類可以被斷言為子類
當類之間有繼承關係時,型別斷言也很常見:
class ApiError extends Error {
code: number = 0;
}
class HttpError extends Error {
statusCode: number = 200;
}
function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}
宣告瞭函式 isApiError
,用來判斷傳入的引數是不是 ApiError
型別,其引數的型別肯定得是父類 Error
,因此該函式便可接受 Error
或它的子類作為引數。
但由於父類 Error
中沒有 code
屬性,故直接獲取 error.code
會報錯,需要使用型別斷言獲取 (error as ApiError).code
。
此案例中有一個更合適的方式來判斷是不是 ApiError
,那就是使用 instanceof
, 因為 ApiError
是一個 JavaScript 的類,能夠通過 instanceof
來判斷 error
是否是它的例項:
class ApiError extends Error {
code: number = 0;
}
class HttpError extends Error {
statusCode: number = 200;
}
function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}
但有些情況下 ApiError
和 HttpError
不是一個真正的類,而只是一個 TS 的介面(interface
),介面是一個型別,不是一個真正的值,它在編譯結果中會被刪除,就無法使用 instanceof
來做執行時判斷了:
interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}
function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}
// 'ApiError' only refers to a type, but is being used as a value here.
此時就只能用型別斷言,通過判斷是否存在 code
屬性,來判斷傳入的引數是不是 ApiError
:
interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}
function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}
任何型別都可以被斷言為 any
理想情況下,每個值的型別都具體而精確,但引用一個該型別上不存在的屬性或方法,就會報錯:
const foo: number = 1;
foo.length = 1;
// 數字型別上是沒有 `length` 屬性
// Property 'length' does not exist on type 'number'.
而有時,非常確定一段程式碼不會出錯,如給 window
上新增一個屬性 foo
,但 TS 編譯時會報錯,提示 window
上不存在 foo
屬性。:
window.foo = 1;
// Property 'foo' does not exist on type 'Window & typeof globalThis'.
此時可以使用 as any
臨時將 window
斷言為 any
型別,在 any
型別的變數上,訪問任何屬性都是允許的。
(window as any).foo = 1;
將一個變數斷言為 any
可以說是解決 TypeScript 中型別問題的最後一個手段。它極有可能掩蓋了真正的型別錯誤,所以如果不是非常確定,就不要使用 as any
。總之,一方面不能濫用 as any
,另一方面也不要完全否定它的作用,需要在型別的嚴格性和開發的便利性之間掌握平衡。
any
可以被斷言為任何型別
在日常的開發中,不可避免的需要處理 any型別的變數,我們可以選擇無視它,也可以選擇改進它,通過型別斷言及時的把 any
斷言為精確的型別,亡羊補牢,提高程式碼可維護性。
function getCacheData(key: string): any {
return (window as any).cache[key];
}
在使用時,最好能夠將呼叫了它之後的返回值斷言成一個精確的型別,方便後續操作:
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
tom.run();
呼叫完 getCacheData
之後,立即將它斷言為 Cat
型別。明確 tom
的型別,後續對 tom
的訪問就有了程式碼補全,提高了程式碼的可維護性。
型別斷言的限制
型別斷言是有限制的,並不是任何一個型別都可以被斷言為任何另一個型別。具體來說,若 A
相容 B
,那麼 A
能夠被斷言為 B
,B
也能被斷言為 A
。
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
let tom: Cat = {
name: 'Tom',
run: () => { console.log('run') }
};
let animal: Animal = tom;
Cat
包含了 Animal
中的所有屬性,TypeScript 只關注最終的結構有什麼關係——所以同 Cat extends Animal
是等價的:
interface Animal {
name: string;
}
interface Cat extends Animal {
run(): void;
}
這也是為什麼 Cat
型別的 tom
可以賦值給 Animal
型別的 animal
。
當 Animal
相容 Cat
時,它們就可以互相進行型別斷言
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
function testAnimal(animal: Animal) {
return (animal as Cat);
}
function testCat(cat: Cat) {
return (cat as Animal);
}
- 允許
animal as Cat
是因為「父類可以被斷言為子類」,這個前面已經學習過了 - 允許
cat as Animal
是因為既然子類擁有父類的屬性和方法,那麼被斷言為父類,獲取父類的屬性、呼叫父類的方法,就不會有任何問題,故「子類可以被斷言為父類」
要使得
A
能夠被斷言為B
,只需要A
相容B
或B
相容A
即可,這也是為了在型別斷言時的安全考慮,畢竟毫無根據的斷言是非常危險的。綜上所述:
- 聯合型別可以被斷言為其中一個型別
- 父類可以被斷言為子類
- 任何型別都可以被斷言為 any
- any 可以被斷言為任何型別
- 要使得
A
能夠被斷言為B
,只需要A
相容B
或B
相容A
即可
其實前四種情況都是最後一個的特例。
雙重斷言
既然:
- 任何型別都可以被斷言為 any
- any 可以被斷言為任何型別
interface Cat {
run(): void;
}
interface Fish {
swim(): void;
}
function testCat(cat: Cat) {
return (cat as any as Fish);
}
若直接使用 cat as Fish
肯定會報錯,因為 Cat
和 Fish
互相都不相容。
但是使用雙重斷言,則可以打破要使得A能夠被斷言為 B,只需A 相容 B 或B相容A即可
的限制,可以使用雙重斷言 as any as Foo
,將任何一個型別斷言為任何另一個型別
。
但是若使用了雙重斷言,那麼很可能會導致執行時錯誤。除非迫不得已,千萬別用雙重斷言。
型別斷言 vs 型別轉換
型別斷言不是型別轉換,它不會真的影響到變數的型別。
型別斷言只會影響編譯時的型別,斷言語句在編譯結果中會被刪除:
function toBoolean(something: any): boolean {
return something as boolean;
}
toBoolean(1);
// 返回值為 1
若要進行型別轉換,還是需要呼叫型別轉換的方法:
function toBoolean(something: any): boolean {
return Boolean(something);
}
toBoolean(1);
// 返回值為 true
上一篇:TypeScript 入門自學筆記(一)