2019-10-28:
學習內容:接口、數組的類型、函數的類型
(展開涉及多個內容)
參考:https://ts.xcatliu.com/basics/type-of-function
一、接口(Interfaces):
(1)
LabelledValue
接口就好比一個名字,用來描述上面例子里的要求。 它代表了有一個 label
屬性且類型為string
的對象。 需要注意的是,我們在這里並不能像在其它語言里一樣,說傳給 printLabel
的對象實現了這個接口。我們只會去關注值的外形。 只要傳入的對象滿足上面提到的必要條件,那么它就是被允許的。
還有一點值得提的是,類型檢查器不會去檢查屬性的順序,只要相應的屬性存在並且類型也是對的就可以。
interface LabelledValue { label: string;
color?: string } function printLabel(labelledObj: LabelledValue) { console.log(labelledObj.label); } let myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
// 可選屬性:接口里的屬性不全都是必需的。 有些是只在某些條件下存在,或者根本不存在。在可選屬性名字定義的后面加一個?
符號。可選屬性的好處之一是可以對可能存在的屬性進行預定義,好處之二是可以捕獲引用了不存在的屬性時的錯誤
(2)只讀屬性:readonly
一些對象屬性只能在對象剛剛創建的時候修改其值。 你可以在屬性名前用 readonly
來指定只讀屬性
interface Point { readonly x: number; readonly y: number; } let p1: Point = { x: 10, y: 20 }; p1.x = 5; // error!
只讀數組類型:
let a: number[] = [1, 2, 3, 4]; let ro: ReadonlyArray<number> = a; ro[0] = 12; // error! ro.push(5); // error! ro.length = 100; // error! a = ro; // error! // 最后一行可以看到就算把整個ReadonlyArray賦值到一個普通數組也是不可以的。解決的方法就是類型斷言重寫(就是沒有辦法)
區別readonly 和 const:const針對變量,readonly針對屬性。
(3)額外的屬性檢查:多數情況下它都是個bug,不建議繞開檢查
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { // ... } let mySquare = createSquare({ colour: "red", width: 100 });
//error: 'colour' not expected in type 'SquareConfig'
注意傳入createSquare
的參數拼寫為colour
而不是color
。 在JavaScript里,這會默默地失敗。對象字面量會被特殊對待而且會經過 額外屬性檢查,當將它們賦值給變量或作為參數傳遞的時候。 如果一個對象字面量存在任何“目標類型”不包含的屬性時,你會得到一個錯誤。
-- 如何繞開檢查?
解決辦法一:
最佳的方式是能夠添加一個字符串索引簽名,前提是你能夠確定這個對象可能具有某些做為特殊用途使用的額外屬性。 如果 SquareConfig
帶有上面定義的類型的color
和width
屬性,並且還會帶有任意數量的其它屬性,那么我們可以這樣定義它:
interface SquareConfig { color?: string; width?: number; [propName: string]: any; }
解決辦法二:
將這個對象賦值給一個另一個變量: 因為 squareOptions
不會經過額外屬性檢查,所以編譯器不會報錯。
let squareOptions = { colour: "red", width: 100 }; let mySquare = createSquare(squareOptions);
(4)用接口描述函數類型:(輸入輸出判斷)
為了使用接口表示函數類型,我們需要給接口定義一個調用簽名。 它就像是一個只有參數列表和返回值類型的函數定義。參數列表里的每個參數都需要名字和類型。對於函數類型的類型檢查來說,函數的參數名不需要與接口里定義的名字相匹配。
interface SearchFunc { (source: string, subString: string): boolean; } let mySearch: SearchFunc; mySearch = function(src: string, sub: string): boolean { let result = src.search(sub); return result > -1; } // 函數的參數會逐個進行檢查,要求對應位置上的參數類型是兼容的。 如果你不想指定類型,TypeScript的類型系統會推斷出參數類型,因為函數直接賦值給了 SearchFunc類型變量。 函數的返回值類型是通過其返回值推斷出來的(此例是 false和true)。 如果讓這個函數返回數字或字符串,類型檢查器會警告我們函數的返回值類型與 SearchFunc接口中的定義不匹配。
(5)可索引的類型:
可索引類型具有一個 索引簽名,它描述了對象索引的類型,還有相應的索引返回值類型。
interface StringArray { [index: number]: string; } // 索引簽名 let myArray: StringArray; myArray = ["Bob", "Fred"]; let myStr: string = myArray[0]; // 我們定義了StringArray接口,它具有索引簽名。 這個索引簽名表示了當用 number去索引StringArray時會得到string類型的返回值。
TypeScript支持兩種索引簽名:字符串和數字。 可以同時使用兩種類型的索引,但是數字索引的返回值必須是字符串索引返回值類型的子類型。 這是因為當使用 number
來索引時,JavaScript會將它轉換成string
然后再去索引對象。 也就是說用 100
(一個number
)去索引等同於使用"100"
(一個string
)去索引,因此兩者需要保持一致。
字符串索引簽名能夠很好的描述dictionary
模式,並且它們也會確保所有屬性與其返回值類型相匹配。 因為字符串索引聲明了 obj.property
和obj["property"]
兩種形式都可以
interface NumberDictionary { [index: string]: number; length: number; // 可以,length是number類型。數字索引的返回值必須是字符串索引返回值類型的子類型 name: string // 錯誤,`name`的類型與索引類型返回值的類型不匹配,應該是number 類型 }
最后,你可以將索引簽名設置為只讀,這樣就防止了給索引賦值:
interface ReadonlyStringArray { readonly [index: number]: string; } let myArray: ReadonlyStringArray = ["Alice", "Bob"]; myArray[2] = "Mallory"; // error! // 你不能設置myArray[2],因為索引簽名是只讀的。
(6)類類型,繼承接口:將在《類與接口》部分補充
二、數組的類型:
回顧:類型+方括號表示法:number[]
數組泛型: Array<number>
(1)用接口表示數組:
NumberArray
表示:只要索引的類型是數字時,那么值的類型必須是數字。
雖然接口也可以用來描述數組,但是我們一般不會這么做,因為這種方式比前兩種方式復雜多了。類數組除外
interface NumberArray { [index: number]: number; }
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
(2)類數組:
類數組不是數組類型!
下例除了約束當索引的類型是數字時,值的類型必須是數字之外,也約束了它還有 length
和 callee
兩個屬性
function sum() { let args: { [index: number]: number; length: number; callee: Function; } = arguments; }
內置對象有IArguments
, NodeList
, HTMLCollection是TS定義好的類型,有自己的接口定義:
function sum() { let args: IArguments = arguments; } interface IArguments { [index: number]: any; length: number; callee: Function; } // Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.
(3)any在數組中的應用:
數組中允許出現任意類型:
let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];
三、函數的類型:(JS對函數式的支持更好)
(1)函數的聲明有兩種:
1、函數聲明:
// 函數聲明(Function Declaration) function sum(x, y) { return x + y; }
2、函數表達式:
// 函數表達式(Function Expression) let mySum = function (x, y) { return x + y; };
(2)一個函數有輸入和輸出,要在 TypeScript 中對其進行約束,需要把輸入和輸出都考慮到:
函數聲明的約束:
function sum(x: number, y: number): number { return x + y; }
注意:不允許超出或少於數量的參數
函數表達式的約束:
let mySum: (x: number, y: number) => number = function (x: number, y: number): number { return x + y; }; // 對等號右側的匿名函數進行了類型定義,而等號左邊的 mySum,是通過賦值操作進行類型推論而推斷出來的。手動給等號左邊添加類型應該這樣
注意:不要混淆了 TypeScript 中的 =>
和 ES6 中的 =>
。
在 TypeScript 的類型定義中,=>
用來表示函數的定義,左邊是輸入類型,需要用括號括起來,右邊是輸出類型。
在 ES6 中,=>
叫做箭頭函數,應用十分廣泛,
(3)用接口定義函數的形狀,見《接口》部分第三part
(4)函數中的可選參數,見《接口》第一part《可選參數》
需要注意的是,可選參數必須接在必需參數后面。換句話說,可選參數后面不允許再出現必需參數了
(5)參數默認值:
在 ES6 中,我們允許給函數的參數添加默認值,TypeScript 會將添加了默認值的參數識別為可選參數,此時就不受「可選參數必須接在必需參數后面」的限制了:
function buildName(firstName: string = 'Tom', lastName: string) { return firstName + ' ' + lastName; } let tomcat = buildName('Tom', 'Cat'); let cat = buildName(undefined, 'Cat');
(6)剩余參數的獲取:
使用 ...rest
的方式獲取函數中的剩余參數(rest 參數):
function push(array: any[], ...items: any[]) { items.forEach(function(item) { array.push(item); }); } let a = []; push(a, 1, 2, 3);
(7)重載:
重載允許一個函數接受不同數量或類型的參數時,作出不同的處理。
比如,我們需要實現一個函數 reverse
,輸入數字 123
的時候,輸出反轉的數字 321
,輸入字符串 'hello'
的時候,輸出反轉的字符串 'olleh'。
這時,我們可以使用重載定義多個 reverse
的函數類型:
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(''); } }
我們重復定義了多次函數 reverse
,前幾次都是函數定義,最后一次是函數實現。在編輯器的代碼提示中,可以正確的看到前兩個提示。
注意,TypeScript 會優先從最前面的函數定義開始匹配,所以多個函數定義如果有包含關系,需要優先把精確的定義寫在前面。