一、類型兼容性
ts 允許類型兼容的變量相互賦值,這個特性增加了語言的靈活性
當一個 類型Y 可以被賦值給另一個 類型X 時,就可以說類型X兼容類型Y。其中,X被稱為“目標類型”,Y被稱為“源類型”
X兼容Y : X(目標類型) = Y(源類型)
1、結構之間兼容:成員少的兼容成員多的
基本規則是,如果 X 要兼容 Y,那么 Y 至少具有與 X 相同的屬性
interface Named { name: string; } let x: Named; let y = { name: 'Chirs', age: 23 }; x = y; console.log('x', x); // x { name: 'Chirs', age: 23 } // 這里要檢查 y 是否可以賦值給 x,編譯器檢查 x 中的每個屬性,看能否在 y 中也找到對應的屬性 // 相反,把 y 賦值給 x 就會報錯,因為 x 不具備 age 屬性 y = x; // Property 'age' is missing in type 'Named' but required in type '{ name: string; age: number; }'
1-1、子類型賦值
let s: string = 'hello'; s = null; // 由於在 ts 中, null 是所有類型的子類型,也就是說 字符類型兼容null類型,所以可以賦值
1-2、接口兼容性
interface X { a: any; b: any; } interface Y { a: any; b: any; c: any; } let x: X = { a: 1, b: '2' } let y: Y = { a: 3, b: 4, c: 5 } // 只要源類型y 具備了 目標類型x 的所有屬性,就可以認為 x 兼容 y x = y; console.log('x', x); // x { a: 3, b: 4, c: 5 }
2
、函數之間兼容:參數多的兼容參數少的
需要判斷函數之間是否兼容,常見於兩個函數相互賦值的情況下,也就是函數作為參數的情況
2-1、如果要目標函數兼容源函數,需要同時滿足三個條件:
(1)、參數個數:目標函數的個數 多余 源函數的個數
interface Handler { (x: number, y: number): void } function foo(handler: Handler) { // handler:目標函數 return handler } let h1 = (a: number) => {} // h1:源函數 // 目標函數的參數個數2個 > 源函數參數個數1個 foo(h1); let h2 = (a: number, b: number, c: number) => {} // h1:源函數 // 目標函數的參數個數2個 < 源函數參數個數3個 foo(h2); // 類型“(a: number, b: number, c: number) => void”的參數不能賦給類型“Handler”的參數
(2)、參數類型:參數類型必須要匹配
interface Handler { (x: number, y: number): void } function foo(handler: Handler) { // handler:目標函數 return handler } let h3 = (a: string) => {} // h3:源函數 // 盡管目標函數的參數個數多余源函數的參數個數,但是參數類型不同 foo(h3); /* 報錯信息: 類型“(a: string) => void”的參數不能賦給類型“Handler”的參數 參數“a”和“x” 的類型不兼容 不能將類型“number”分配給類型“string” */
interface Point3D { x: number; y: number; z: number; } interface Point2D { x: number; y: number; } // 函數 p3d 和 p2d 的參數個數都是1,參數類型都是對象 let p3d = (point: Point3D) => {} let p2d = (point: Point2D) => {} // 賦值時,依然采用的是目標函數的參數個數必須大於源函數參數個數,且參數類型相同的原則 p3d = p2d; p2d = p3d; // 想要不報錯,需要關閉 tsconfig.json 中的一個配置 strictFunctionTypes
函數的參數之間可以相互賦值的情況,稱為 “
函數參數雙向協變”。它允許把一個精確的類型,賦值給一個不那么精確的類型,這樣就不需要把一個不精確的類型斷言成一個精確的類型了
(3)、返回值類型:目標函數的返回值類型必須與源函數的返回值類型相同,或為其子類型
let p = () => ({ name: 'Bob' }) let s = () => ({ name: 'Bob', age: 23 }) // p 作為目標函數,s 作為源函數時,目標函數的返回值是源函數返回值的子類型 p = s; s = p; // 不能將類型“() => { name: string; }”分配給類型“() => { name: string; age: number; }”
2-2、關於固定參數、可選參數和剩余參數之間的兼容
1)、固定參數可以兼容可選參數和剩余參數
2)、可選參數不兼容固定參數和剩余參數
3)、剩余參數可以兼容固定參數和剩余參數
// 固定參數 let a = (x: number, y: number) => {}; // 可選參數 let b = (x?: number, y?: number) => {}; // 剩余參數 let c = (...args: number[]) => {}; // 固定參數 兼容 可選參數和剩余參數 a = b; a = c; // 可選參數 不兼容 固定參數和剩余參數 (可將 strictFunctionTypes 設為false 實現兼容) b = a; b = c; // 剩余參數 兼容 固定參數和可選參數 c = a; c = b;
2-3、函數重載
對於有重載的函數,源函數的每個重載都要在目標函數上找到對應的函數簽名,這樣確保了目標函數可以在所有源函數可調用的地方地方
// 源函數 function overload(x: number, y: number): number; function overload(x: string, y: string): string; // 目標函數 function overload(x: any, y: any): any{ };
// Error1: 目標函數的參數個數 少於 源函數的參數 // 源函數 function overload(x: number, y: number): number; // This overload signature is not compatible with its implementation signature function overload(x: string, y: string): string; // 目標函數 function overload(x: any, y: any, z: any): any{ }; // Error2: 目標函數和源函數的返回值類型不兼容 // 源函數 function overload(x: number, y: number): number; // This overload signature is not compatible with its implementation signature function overload(x: string, y: string): string; // 目標函數 function overload(x: any, y: any) { };
3、枚舉類型的兼容性
(1)、枚舉類型和數字類型相互兼容
(2)、枚舉類型之間是完全不兼容的
enum Color { Red, Green, Pink }; enum Fruit { Apple, Banana, Orange }; // 枚舉類型和數字類型相互兼容 let fruit: Fruit.Apple = 4; let num: number = Color.Red; // 相同枚舉類型之間不兼容 let c: Color.Green = Color.Red; // 不能將類型“Color.Red”分配給類型“Color.Green” // 不同枚舉類型之間不兼容 let color: Color.Pink = Fruit.Orange; // 不能將類型“Fruit.Orange”分配給類型“Color.Pink”
4、類兼容性
(1)、靜態成員和構造函數是不參與比較的,如果兩個類具有相同的實例成員,那他們的實例則可以兼容
class A { id: number = 1; constructor(p: number, q: number) {} } class B { static s: number = 1; id: number = 2; constructor(p: number) {} } let aa = new A(3, 6); let bb = new B(8); // 兩個類都含有相同的實例成員 number 類型的id,盡管構造函數不同,依然相互兼容 aa = bb; bb == aa;
(2)、如果兩個類中含有相同的私有成員,他們的實例不兼容,但是父類和子類的實例可以相互兼容
class A { id: number = 1; private name: string = 'hello'; constructor(p: number, q: number) {} } class B { static s: number = 1; id: number = 2; private name: string = 'hello'; constructor(p: number) {} } let aa = new A(3, 6); let bb = new B(8); // 在上例的基礎上各自添加了相同的 私有成員name,就無法兼容了 aa = bb; bb == aa; // 均報錯:不能將類型“B”分配給類型“A”,類型具有私有屬性“name”的單獨聲明
class A { id: number = 1; private name: string = 'hello'; constructor(p: number, q: number) {} } class SubA extends A {} let aa = new A(3, 6); let child = new SubA(1, 2) // 就算包含私有成員屬性,但是父類和子類的實例可以相互兼容 aa = child; child == aa;
5、泛型兼容性
(1)、如果兩個泛型的定義相同,但是沒有指定泛型參數,它們之間也是相互兼容的;
// demo 1 interface Empty<T> {}; let a: Empty<string> = {}; let b: Empty<number> = {}; a = b; b = a; // demo 2 let log1 = <T>(x: T): T => { console.log('x'); return x } let log2 = <U>(y: U): U => { console.log('y'); return y; } log1 = log2;
(2)、如果泛型中指定了類型參數,會按照結果類型進行比較;
interface NotEmpty<T> { value: T; }; let a: NotEmpty<string> = { value: 'string' }; let b: NotEmpty<number> = { value: 123 }; a = b; // 不能將類型“NotEmpty<number>”分配給類型“NotEmpty<string>”
二、類型保護
此處定義了一個枚舉Type 和兩個類,兩個類都有打印的方法,在 getLanguage 函數中,我們希望通過傳入不同的參數,調用對應的打印方法
enum Type { Strong, Weak } class Java { helloJava() { console.log('Hello Java') } } class JavaScript { helloJavaScript() { console.log('Hello JavaScript') } } function getLanguage(type: Type) { let lang = type === Type.Strong ? new Java() : new JavaScript(); // Error:類型“Java | JavaScript”上不存在屬性“helloJava” if(lang.helloJava) { lang.helloJava() // Error:類型“JavaScript”上不存在屬性“helloJava” } else { lang.helloJavaScript() // Error:類型“Java”上不存在屬性“helloJavaScript” } return lang; }
事實上,在上例中,變量lang被認為是一個聯合類型,意味着它必須同時具有 helloJava 和 helloJavaScript 兩個方法。此處為了解決報錯,就需要借助 類型斷言
function getLanguage(type: Type) { let lang = type === Type.Strong ? new Java() : new JavaScript(); // 使用類型斷言 if((lang as Java).helloJava) { (lang as Java).helloJava() } else { (lang as JavaScript).helloJavaScript() } return lang; } getLanguage(Type.Strong); // Hello Java
由於不知道會傳入什么樣的參數,因此必須在每一處都加上類型斷言。顯然,這並不是一個理想的解決方案,代碼變得冗長且代碼的可讀性很差。
類型保護就是用來解決這個問題的,它可以提前對類型進行預判。
1、什么是類型保護
TypeScript 能夠在特定的區塊中保護變量屬於某種確定的類型,可以在此區塊中放心的引用此類型的屬性,或者調用此類型的方法。
2、創建特定區塊的方法:
(1)、instanceOf 判斷一個實例是不是屬於某個類
function getLanguage(type: Type) { let lang = type === Type.Strong ? new Java() : new JavaScript(); // instanceOf if(lang instanceof Java) { lang.helloJava() } else { lang.helloJavaScript() } return lang; }
(2)、in 判斷一個屬性是不是屬於某個對象
enum Type { Strong, Weak } // 添加一個實例屬性,同時要添加構造器,否則在實例對象上還是找不到那個屬性 class Java { java: any; constructor(java: any) { this.java = java; } helloJava() { console.log('Hello Java') } } class JavaScript { js: any; constructor(js: any) { this.js = js; } helloJavaScript() { console.log('Hello JavaScript') } } function getLanguage(type: Type) { let lang = type === Type.Strong ? new Java('java') : new JavaScript('js'); // in if('java' in lang) { lang.helloJava() } else { lang.helloJavaScript() } return lang; } getLanguage(Type.Strong); // Hello Java
(3)、typeof 判斷一個變量的類型
function getLanguage(x: string | number) { // typeof:此處只是提供一種創建類型保護區塊的方法,並不解決此例中的問題 if(typeof x === 'string') { console.log(x.length) } else { console.log(x.toFixed(2)); } }
(4)、類型保護函數 某些判斷可能不是一條語句能夠搞定的,需要更多復雜的邏輯,適合封裝到一個函數內
enum Type { Strong, Weak } class Java { helloJava() { console.log('Hello Java') } } class JavaScript { helloJavaScript() { console.log('Hello JavaScript') } } // 注意類型保護的返回值,是一個“類型謂詞” function isJava(lang: Java | JavaScript): lang is Java { return (lang as Java).helloJava !== undefined } function getLanguage(type: Type) { let lang = type === Type.Strong ? new Java() : new JavaScript(); // 類型保護函數 if(isJava(lang)) { lang.helloJava() } else { lang.helloJavaScript() } return lang; } getLanguage(Type.Strong); // Hello Java