一、混入(mixins)
mixins 混入可以理解為擴(kuò)展,把本來不屬于自己的東西硬生生的給弄到自己身上,在 TypeScript 中主要分為類的混入和對象的混入。
mixins 與 extends 如何區(qū)分?
繼承是上下級關(guān)系,混入是平級關(guān)系,例如人和狗都屬于(繼承)動物類,但是人和狗都有吃這個方法,如果這個方法你在人里面定義了,就可以混入到狗里面。
1.1 對象的混入 mixins
JavaScript 對象的混入,把 b 的內(nèi)容混入 a 并賦值給 c ,a 得到擴(kuò)展有 b 的屬性和方法。
var a = {name:"name"};
var b = {age:18};
var c = Object.assign(a,b);
console.log(a,b,c);
//{name: "name", age: 18} {age: 18} {name: "name", age: 18}
上面的混入我們擴(kuò)展了 a ,但是大多數(shù)情況下我們不會去直接擴(kuò)展 a ,所以上面的混入我們都是這樣寫。
var c = Object.assign({},a,b);
TypeScript 中的混入:
interface ObjectA{
a:string;
}
interface ObjectB{
b:number;
}
var a:ObjectA = {a:"a"};
var b:ObjectB = {b:12};
var c = Object.assign(a , b);//c的類型交叉類型為ObjectA & ObjectB
TypeScript 混入之后得到的值根據(jù)類型推論是交叉類型。
1.2 類的混入
ES5 的混入:
function A() {};
A.prototype.eat = function(){};
function B() {};
A.prototype.run = function(){};
const mixins = (target,from)=>{
Object.keys(from).forEach(key=>{
console.log(from[key],target[key]);
target[key] = from[key];
});
};
mixins(A.prototype , B.prototype);
console.log(A.prototype);
//{eat: ?, run: ?, constructor: ?}
ES6 的混入
class A {
eat(){}
}
class B {
run(){}
}
const mixins = (target,from)=>{
Object.getOwnPropertyNames(from).forEach(key=>{
console.log(from[key],target[key]);
target[key] = from[key];
});
};
mixins(A.prototype , B.prototype);
console.log(A.prototype);
//{eat: ?, run: ?, constructor: ?}
ES5 之所以和 ES6 混入寫法不同在于,ES6 類的內(nèi)部所有定義的方法,都是不可枚舉的(non-enumerable),但是 ES5 是可以的。所以:
Object.getOwnPropertyNames 和 Object.keys 的區(qū)別?
即 Object.keys 只適用于可枚舉的屬性,而 Object.getOwnPropertyNames 返回對象自動的全部屬性名稱。
TypeScript 版混入:
class A {
public isA: boolean;
public funA() { }
}
class B {
public isB: boolean;
public funB() { }
}
class AB implements A , B {
public isA: boolean = false;
public isB: boolean = false;
public funA: () => void;
public funB: () => void;
}
applyMixins(AB, [A, B]);
function applyMixins(base: any, from: any[]) {
from.forEach(fromItem => {
Object.getOwnPropertyNames(fromItem.prototype).forEach(key => {
Object.defineProperty(base.prototype, key, Object.getOwnPropertyDescriptor(fromItem.prototype , key ));
});
});
}
console.log(AB.prototype);
Object.getOwnPropertyDescriptor(obj,key);的作用就是能夠獲取對象的key描述可以完整作為 Object.defineProperty方法的第三個參數(shù)。
var obj = { "name":"Condor Hero" };
var desc = Object.getOwnPropertyDescriptor (obj, "name" );
console.log(desc);
{
value: 'Condor Hero',
writable: true,
enumerable: true,
configurable: true
}
其實類的混入最簡單的寫法是:
Object.assign(A.prototype , B.prototype);
二、交叉類型(Intersection Types)和聯(lián)合類型(Union Types)
2.1 交叉類型(Intersection Types)是將多個類型合并為一個類型。
這讓我們可以把現(xiàn)有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。 例如,Person & Serializable & Loggable同時是Person和Serializable和Loggable。 就是說這個類型的對象同時擁有了這三種類型的成員。
我們大多是在混入(mixins)或其它不適合典型面向?qū)ο竽P偷牡胤娇吹浇徊骖愋偷氖褂谩?(在JavaScript里發(fā)生這種情況的場合很多!) 下面是如何創(chuàng)建混入的一個簡單例子("target": "es5"):
function extend<First, Second>(first: First, second: Second): First & Second {
const result: Partial<First & Second> = {};
for (const prop in first) {
if (first.hasOwnProperty(prop)) {
(<First>result)[prop] = first[prop];
}
}
for (const prop in second) {
if (second.hasOwnProperty(prop)) {
(<Second>result)[prop] = second[prop];
}
}
return <First & Second>result;
}
class Person {
constructor(public name: string) { }
}
interface Loggable {
log(name: string): void;
}
class ConsoleLogger implements Loggable {
log(name) {
console.log(`Hello, I'm ${name}.`);
}
}
const jim = extend(new Person('Jim'), ConsoleLogger.prototype);
jim.log(jim.name);
這個代碼沒看懂。。。
2.2 聯(lián)合類型(Union Types)
一個變量希望傳入 number 或 string 類型的參數(shù),首先使用聯(lián)合類型。 聯(lián)合類型表示一個值可以是幾種類型之一。 我們用豎線(|)分隔每個類型,所以 number | string | boolean
表示一個值可以是 number,string,或 boolean。
var a = number | string | boolean;
三、類型推論
3.1 基礎(chǔ)
TypeScript 中沒有明確指定類型的地方,類型推論會嘗試給出類型,比如:
const x = 3;
此時,類型推論會發(fā)生在初始化變量和成員的時候、設(shè)置默認(rèn)參數(shù)值和決定函數(shù)的返回值的時候,變量 x 類型被推斷為 number
3.2 最佳通用類型
當(dāng)需要從幾個表達(dá)式中推斷類型的時候,會使用這些表達(dá)式的類型來推斷出一個最合適的通用類型,比如:
let x = [0, 1, null];
此時為了推斷 x 的類型,需要遍歷整個數(shù)組的所有元素的類型。從遍歷結(jié)果看有兩個選擇,分別是 number
和 null
。計算通用類型算法會考慮所有的候選類型,并給出一個兼容所有候選類型的類型。
因為最終的通用類型也是來自候選類型,有時候候選類型共享相同的通用類型,但是卻沒有一個類型能夠為所有候選類型所兼容,比如:
let zoo = [new Rhino(), new Elephant(), new Snake()];
上面的數(shù)組中,如果有個 Animal
,并且是上面三個 class 的基類,則可以作為最佳通用類型。但是沒有找到最佳通用類型,類型推斷的結(jié)果成為聯(lián)合數(shù)組類型:(Rhino | Elephant | Snake)[]
3.3 上下文類型
TypeScript 的類型推斷也可能按照相反的方向進(jìn)行,稱為 按上下文歸類
。按上下文歸類會發(fā)生在表達(dá)式的類型與所處的位置相關(guān)時。
window.onmousedown = function(mouseEvent: number) {
console.log(mouseEvent.button); //<- Error
};
上面代碼中會報錯如下:
而此時如果將 mouseEvent
類型聲明為 any
則是 OK 的,因為對于 window.onmousedown
根據(jù)上下文推斷 mouseEvent
參數(shù)的類型不可能是 number,如果不聲明 mouseEvent 的類型,則會推斷成 any
,而此時如果聲明是 any 則也不會報錯。
window.onmousedown = function(mouseEvent: any) {
console.log(mouseEvent.button);
};
如果明確聲明了參數(shù)類型,則會忽略上下文的類型
上下文類型會在很多情況下使用到。 通常包含函數(shù)的參數(shù)、賦值表達(dá)式的右邊、類型斷言、對象成員和數(shù)組字面量和返回值語句。 上下文類型也會做為最佳通用類型的候選類型。比如:
function createZoo(): Animal[] {
return [new Rhino(), new Elephant(), new Snake()];
}
上面從上下文中推斷出了 Animal 是最合適的最佳候選類型。
四、聲明合并
在 JavaScript 中,如果使用 var 定義了同名變量,后者會覆蓋前者,let 和 const 不允許出現(xiàn)同名變量。在 TypeScript 中,聲明合 是將對同一個名字的兩個獨立聲明合并為單一聲明。 合并后的聲明同時擁有原先兩個聲明的特性。 任何數(shù)量的聲明都可被合并;不局限于兩個聲明。
4.1 同類型的聲明合并
假設(shè)定義了兩個相同名字的函數(shù)、接口、命名空間或類,看看這種同類型的聲明合并成為一種類型的情況:
- 函數(shù)的合并
兩個相同名字的函數(shù)會發(fā)生函數(shù)重載,我們可以使用重載定義多個函數(shù)類型:
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
- 接口的合并
接口中的屬性在合并時會簡單的合并到一個接口中:
interface Alarm {
price: number;
}
interface Alarm {
weight: number;
}
相當(dāng)于:
interface Alarm {
price: number;
weight: number;
}
注意,合并的屬性的類型必須是唯一的:
interface Alarm {
price: number;
}
interface Alarm {
price: number; // 雖然重復(fù)了,但是類型都是 `number`,所以不會報錯
weight: number;
}
interface Alarm {
price: number;
}
interface Alarm {
price: string; // 類型不一致,會報錯
weight: number;
}
// index.ts(5,3): error TS2403: Subsequent variable declarations must have the same type. Variable 'price' must be of type 'number', but here has type 'string'.
接口中方法的合并,與函數(shù)的合并一樣:
interface Alarm {
price: number;
alert(s: string): string;
}
interface Alarm {
weight: number;
alert(s: string, n: number): string;
}
相當(dāng)于
interface Alarm {
price: number;
weight: number;
alert(s: string): string;
alert(s: string, n: number): string;
}
- 類的合并
類的合并與接口的合并規(guī)則一致。 - 命名空間的合并
命名空間的合并與接口的合并規(guī)則一致,命名空間共享出去的是 export 出去的東西。
4.2 不同類型之間的合并
- 命名空間和類(函數(shù))
命名空間和類,類一定的在命名空間前面,合并的最終結(jié)果為類(函數(shù))。
class Validations { };
namespace Validations {
export const a:number = 10;
};
console.log(Validations.a);//10
合并規(guī)則是命名空間 export 導(dǎo)出的東西,將會作為類的靜態(tài)屬性。所以命名命名空間可以為類增加靜態(tài)屬性。
- 命名空間和枚舉
書寫沒有順序要求。
enum Color { blue , green , white};
namespace Color { export const yellow = 99 };
console.log(Color);//{0: "blue", 1: "green", 2: "white", blue: 0, green: 1, white: 2, yellow: 99}
五、類型兼容性
基本沒啥用,你要用這個就是自己給自己找麻煩。類型兼容性主要涉及的就是類、函數(shù)、接口等等,我們在使用時候的規(guī)范,只要正常使用就沒什么事情。
5.1 對象兼容
如果,我們定義一個包含屬性 name 的對象
interface Name {
name : string
}
let obj : Name;
這樣我們定義 obj 的時候就只能并且必須使 obj 只有一個 name 屬性,不能多也不能少。所以,對 obj 的賦值只能是:
obj = {
name : 'heshen',
}
但是,如果我們只是定義了這種類型的變量但是并沒有賦值,但又保不齊以后會對 obj 賦值,可是我們不能保證對 obj 賦值的對象是不是符合Name,怎么辦?其實 TypeScript 是自帶類型兼容,它會檢查賦值的變量是否包含 name 屬性,如果包含 name 屬性則 obj 兼容 myname 例如:
interface Name {
name : string
}
let obj : Name;
let myname = {
name : 'heshen',
height : 180
}
obj = myname;
雖然,myname 是有 height 屬性,但是也含有 name 屬性,所以根據(jù) TypeScript 結(jié)構(gòu)化類型系統(tǒng)的規(guī)則,obj 兼容 myname。如果要是反過來是否也成立呢?
interface Name {
name : string;
age : number
}
let obj : Name;
let myname = {
name : 'cxh'
}
obj = myname;
obj 需要 name ,age 兩個屬性,但是 myname 只有一個 name,編譯器遍歷 myname,發(fā)現(xiàn)并沒有找到age,所以肯定報錯了。
現(xiàn)在給 interface 自由增加屬性又多了一個方法,那就是類型兼容,這是第三個方法了。
上面提到結(jié)構(gòu)類型系統(tǒng),那什么是結(jié)構(gòu)類型系統(tǒng),又什么是名義(nominal)類型系統(tǒng)?
interface Named {
name: string;
}
class Person {
name: string;
// strictPropertyInitialization 檢查這里會報錯誤,因為沒有在 constructor 初始化
}
let p: Named = new Person();
// 可行,因為這是一個結(jié)構(gòu)類型
在傳統(tǒng)的面向?qū)ο蟮恼Z言中(比如 C# 或者 Java)上面代碼會報錯誤,因為沒有明確聲明 Person 與 Named 的關(guān)系,Person 沒有實現(xiàn) Named 接口,這就是名義結(jié)構(gòu)系統(tǒng),必須產(chǎn)生關(guān)系。結(jié)構(gòu)類型系統(tǒng)表示,只要數(shù)據(jù)結(jié)構(gòu)相同就正確。
5.2 函數(shù)兼容
判斷基礎(chǔ)類型或者是對象格式的類型還是比較容易判斷出來,關(guān)鍵在于如何判斷兩個函數(shù)是兼容的。
let funcA = (a: number) => 0;
let funcB = (b: number, s: string) => 1;
funcB = funcA; // ok
funcA = funcB; // 報錯
比較函數(shù)主要是比較它們的參數(shù)列表能否兼容,形參的名稱是不重要的,重要的是順序和類型。
將 funcA 賦給 funcB 是沒問題的, 因為 funcB 的參數(shù)足以兼容 funcA 的參數(shù),但是將 funcB 賦給 funcA 是不行的,funcA 無法兼容多的 s 參數(shù)。
funcB = funcA 這樣的形式是 OK 的,因為 JavaSript 本身就經(jīng)常忽略額外的參數(shù),但是缺少參數(shù)是不行的。
函數(shù)的兼容除了比較參數(shù)之外,還會比較返回值類型。
let funcA = () => ({name: 'name'});
let funcB = () => ({name: 'name', age: 18});
funcA = funcB; // ok
funcB = funcA; // 報錯
上面 funcB=funcA 會報錯,因為 funcA 無法兼容 funcB 的返回值中的 age 屬性。類型系統(tǒng)強制源函數(shù)的返回值類型必須是目標(biāo)函數(shù)返回值類型的子類型。
函數(shù)參數(shù)雙向協(xié)變
當(dāng)比較函數(shù)參數(shù)類型時,只有當(dāng)源函數(shù)參數(shù)能夠復(fù)制給目標(biāo)函數(shù)或者反過來的時候才能賦值成功。這是不穩(wěn)定的,因為調(diào)用者可能傳入了一個具有更精確類型信息的函數(shù),但是調(diào)用這個傳入的函數(shù)的時候卻是用了不是那么精確的類型信息。
var funA = function(argv:string | number):void{};
var funB = function(argv:string):void{};
// funA = funB;
funB = funA;
可選參數(shù)及剩余參數(shù)
比較函數(shù)的兼容性的時候,可選參數(shù)和必選參數(shù)是可以互換的。源類型上有額外的可選參數(shù)不是錯誤,目標(biāo)類型的可選參數(shù)在源類型里沒有對應(yīng)的參數(shù)也不是錯誤。
當(dāng)一個函數(shù)有 rest 參數(shù)時,它被當(dāng)做無限個可選參數(shù)。
這對于類型系統(tǒng)來說是不穩(wěn)定的,但從運行時的角度來看,可選參數(shù)一般來說是不強制的,因為對于大多數(shù)函數(shù)來說相當(dāng)于傳遞了一些undefinded。
函數(shù)接收一個回調(diào)函數(shù),而對于程序員來說是可預(yù)知的參數(shù),但對類型系統(tǒng)來說是不確定的參數(shù)來調(diào)用:
function invokeLater(args: any[], callback:(...args: any[]) => void) {}
invokeLater([1,2], (x,y) => { console.log(x +' '+y) });
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));
函數(shù)重載
對于重載的函數(shù),源函數(shù)的每個重載都要在目標(biāo)函數(shù)上找到對應(yīng)的函數(shù)名。確保了目標(biāo)函數(shù)可以在所有源函數(shù)可調(diào)用的地方調(diào)用。
5.3 枚舉
枚舉類型與數(shù)字類型兼容,并且數(shù)字類型與枚舉類型兼容。不同枚舉類型之間是不兼容的。比如,
enum Status {Reday, Waiting};
enum Color {Red, Blue, Green};
Status.Waiting = 10;//ok
let s= Status.Ready;
s = Color.Red; // 報錯
5.4 類
類與對象字面量和接口差不多,但有一點不同:類有靜態(tài)部分和實例部分的類型。 比較兩個類類型的對象時,只有實例的成員會被比較。 靜態(tài)成員和構(gòu)造函數(shù)不在比較的范圍內(nèi)。
class Animal {
feet: number;
constructor(name: string, numFeet: number) { }
}
class Size {
feet: number;
constructor(numFeet: number) { }
}
let a: Animal;
let s: Size;
a = s; // OK
s = a; // OK
類的私有成員和受保護(hù)成員?
類的私有成員和受保護(hù)成員會影響兼容性。當(dāng)檢查類的實例的兼容的時候,如果目標(biāo)類型包含一個私有成員,那么源類型必須包含來自同一個類的這個私有成員。同樣地,這條規(guī)則也適用于包含受保護(hù)成員實例的類型檢查。 這允許子類賦值給父類,但是不能賦值給其它有同樣類型的類。
5.5 泛型
因為 TypeScript 是結(jié)構(gòu)性的類型系統(tǒng),類型參數(shù)只影響使用其做為類型一部分的結(jié)果類型。比如:
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // OK 因為 y 能夠匹配 x 的結(jié)構(gòu)
上面代碼沒有指定具體的接口成員,因此此時 x 和 y 結(jié)構(gòu)類型其實是相同的。
但是如果此時加了一個成員:
interface Empty<T> {
name: T
}
let x: Empty<number>;
let y: Empty<number>;
y = x; // 報錯
對于沒指定泛型類型的泛型參數(shù)時,會把所有泛型參數(shù)當(dāng)成any比較。 然后用結(jié)果類型進(jìn)行比較,就像上面第一個例子。
六、類型別名
類型別名用來給一個類型起個新名字。
type name = string;
var a:name = "字符串類型";