TypeScript躬行記(5)——型別相容性
TypeScript是一種基於結構型別的語言,可根據其成員來描述型別。以結構相同的Person介面和Programmer類為例,如下所示。
interface Person { name: string; } class Programmer { name: string; } let person: Person = new Programmer();
由於結構型別的關係,因此當變數宣告為Person型別時,可通過Programmer類例項化。由此可知,結構型別只關注型別的組成結構,而名稱並不重要。
一、函式
在判斷兩個函式的相容性時,需要考慮引數數量、返回值型別等多個方面。
1)引數數量
假設有兩個函式add()和sum(),它們的引數數量不同,後者比前者多一個引數,而函式的返回值型別相同,如下所示。
let add = (x: number) => 0; let sum = (y: number, z: number) => 0;
當把add賦給sum時,編譯能成功執行;而反之,則會報錯,如下所示。
sum = add; //正確 add = sum; //錯誤
由此可知,引數少的函式不能賦給引數多的;而反過來時,需要確保每個位置所對應的引數型別保持一致,不過引數名稱可以不同。
2)返回值型別
假設有兩個函式add()和sum(),它們沒有引數,後者的返回值比前者多一個屬性,如下所示。
let add = () => ({ x: 1 }); let sum = () => ({ x: 1, y: 2});
當把sum賦給add時,編譯能成功執行;而反之,則會報錯,如下所示。
add = sum; //正確 sum = add; //錯誤
由此可知,源函式的返回值型別得是目標函式返回值的子型別。
3)引數型別
TypeScript中的引數型別需要同時滿足協變和逆變,即雙向協變。協變比較好理解,是指子型別相容父型別,而逆變正好與協變相反。在下面的示例中,定義了父類Person和子類Programmer,Programmer類覆蓋了Person類中的work()方法,並且其引數型別宣告的更加寬泛。
class Person { work(msg: string | undefined) { } } class Programmer extends Person { work(msg: string) { } }
接下來宣告兩個函式,它們的引數型別分別是Person和Programmer兩個類,如下所示,其中person()函式是programmer()函式的子型別。
let person = (x: Person) => 0; let programmer = (x: Programmer) => 0;
由於引數型別是雙向協變的,因此兩個變數之間可相互賦值,如下所示。
person = programmer; programmer = person;
4)可選引數和剩餘引數
在比較函式的相容性時,不需要匹配可選引數。以下面的pls()和add()兩個函式為例,pls()中的兩個引數必傳,而add()中的第二個引數是可選的。
let pls = (x: number, y: number) => 0; let add = (x: number, y?: number) => 0; pls = add; add = pls;
雖然引數不同,但是兩個函式仍然是相容的,並且可以相互賦值。剩餘引數相當於無限個可選引數,也不會被匹配。下面示例中的sum()函式只聲明瞭剩餘引數,它與pls()和add()兩個函式都是相容的。
let sum = (...args: number[]) => 0; pls = sum; sum = pls; add = sum; sum = add;
5)函式過載
當比較存在多個過載的函式時,其每個過載都要在目標函式上找到對應的函式簽名,以此確保目標函式能在源函式所有可呼叫的地方呼叫,如下所示。
interface add { (x: number, y: string): any; (x: number, y: number): number; } function sum(x: number, y: string): any; function sum(x: number, y: number): number; let func: add = sum;
二、列舉
來自於不同列舉型別的列舉值,被認為是不相容的,如下所示,當把Direction.Up賦給color變數時,在編譯階段會報錯。
enum Color { Red, Green, Blue } enum Direction { Up, Down, Left, Right } let color = Color.Red; //正確 color = Direction.Up; //錯誤
數字列舉和數字型別相互相容,如下所示,color變數被賦予了列舉成員,digit變數是一個數字,它們之間可以相互賦值。
let color = Color.Red; let digit = 1; color = digit; digit = color;
字串列舉無法相容字串型別,如下所示,當把field變數賦給Color的列舉成員時,在編譯階段會報錯,但反過來可以正確執行。
enum Color { Red = "RED", Green = "GREEN", Blue = "BLUE" } let color = Color.Red; let field = "PURPLE"; color = field; //錯誤 field = color; //正確
三、類
類與物件字面量和介面類似,但類包含靜態和例項兩部分。在比較兩個類例項時,僅匹配它們的例項成員,而靜態成員和建構函式不影響相容性,因為它們在比較時會被忽略。
在下面的示例中,建立了Person和Programmer兩個類,雖然Programmer類包含了一個靜態屬性,並且其建構函式與Person類不同,但是它們之間可以相互相容。
class Person { name: string; constructor(name: string) { } } class Programmer { name: string; static age: number; constructor(name: string, age: number) { } } let person: Person; let programmer: Programmer; person = programmer; programmer = person;
類的私有成員和受保護成員會影響相容性,TypeScript要求它們必須來源於同一個類,從而既能保證父類相容子類,也能避免與其它相同結構的類相容。
在下面的示例中,Person和Teacher兩個類都包含一個同名的私有屬性,Programmer是Person的子類,三個變數的型別對應這三個類。
class Person { private name: string; } class Teacher { private name: string; } class Programmer extends Person { } let person: Person; let programmer: Programmer; let teacher: Teacher;
person和programmer兩個變數可相互賦值,因為它們的私有成員來源於同一個類,如下所示。
person = programmer; programmer = person;
雖然person和teacher兩個變數的結構相同,但是它們的私有成員來源於兩個不同的類,因此無法相互賦值,如下所示。
person = teacher; //錯誤 teacher = person; //錯誤
四、泛型
當泛型介面中的型別引數未使用時,不會影響其相容性,如下所示,x和y兩個變數可相互賦值。
interface Person<T> { } let x: Person<number>; let y: Person<string>; x = y; y = x;
當泛型介面中的型別引數被一個成員使用時,就會影響其相容性,如下所示,x和y兩個變數不可相互賦值。
interface Person<T> { data: T; } let x: Person<number>; let y: Person<string>; x = y; //錯誤 y = x; //錯誤
當比較未指定引數型別的泛型函式時,在檢查相容性之前會將其替換成any型別,例如下面的兩個函式,相當於對“(x: any)=>any”和“(y: any)=>any”進行匹配,因此可相互賦值。
let send = function<T>(x: T): T { return x; } let func = function<U>(y: U): U { return y; } send = func; func = send;
泛型類的相容性規則與之前所述一