TypeScript 的 Substitutability
- 2021 年 2 月 5 日
- 筆記
- typescript
Substitutability
中文含義是 可代替性,這個詞我未在 TypeScript
的語言特性相關文檔上看到,百度、Google搜索也寥寥無幾。僅在TypeScript FAQ 找到相關描述。
有關類型系統的許多答案都提到了可替代性。 這是一個原則,即如果可以使用對象
X
代替某些對象Y
,則X
是Y
的子類型。我們通常也說X
可以分配給Y
(這些術語在TypeScript
中的含義略有不同,但是 區別在這裡並不重要)。
這段描述很好理解,大體就是子類型可以用在父類型出現的地方。但實際涉及的TypeScript
使用場景,和這個詞不是很契合,也許是語言的差異,中文含義不便於理解。
實際 Substitutability
解決的場景是:TypeScript
允許 function
作為回調函數時,入參個數、返回類型可以不符合方法簽名。
回調 Function 入參比簽名少
fetchResults
有一個參數,即回調函數。 該方法從某處獲取數據,然後執行回調。 回調的方法簽名有兩個參數, statusCode
和results
。
function fetchResults(callback: (statusCode: number, results: number[]) => void) {
const results = [1,2,3];
...
callback(200, results);
}
我們用下面的方式調用 fetchResults
,注意方法簽名是不同的,它沒有第二個參數 results
。
function handler(statusCode: number) {
// 業務處理
...
}
fetchResults(handler); // ✔️
可以正常編譯,沒有任何錯誤或警告。 看起來有點奇怪,但細想一下,你一直在這麼用。
Array.prototype.forEach
方法簽名
/**
* Performs the specified action for each element in an array.
* @param callbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array.
* @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
*/
forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void
實際使用:
let items = [1, 2, 3];
items.forEach(arg => console.log(arg));
在運行時,forEach使用三個參數(value、index、array)調用給定的回調函數,但大多數時候回調函數只使用其中的一個或兩個參數。
那為什麼不幹脆將forEach 參數聲明為可選。
forEach(callback: (element?: T, index?: number, array?: T[]))
如果聲明為可選,由於回調的提供者不知道調用方何時會傳遞多少參數,將不得不檢查各個參數,這顯然不是你想要的。
function maybeCallWithArg(callback: (x?: number) => void) {
if (Math.random() > 0.5) {
callback();
} else {
callback(42);
}
}
聲明非可選,是站在調用者的角度,保證按聲明傳遞參數,可以兼容需要不同個數參數的回調函數;
這麼處理的合理性在於,回調函數自身是最了解如何處理入參的,如果它不關心某些入參,它可以安全的忽略。
回調返回類型不匹配簽名 return void
如果函數類型指定返回類型 void,則也接受具有不同的、更具體的返回類型的函數。同樣,用前面的例子,這次增加handle的返回類型聲明。
function handler(statusCode: number): {age:number}{
// 業務處理
...
return {"age": 4};
}
fetchResults(handler); // ✔️
fetchResults 接受的回調函數返回類型是void,而這次的handler 返回{age:number}
類型,依然正常編譯。
你依然可以將callback結果賦值給一個變數,但僅僅限於聲明語句,其他操作都將編譯失敗。
function fetchResults(callback: (statusCode: number, results: number[]) => void) {
const results = [1,2,3];
...
let didItWork = callback(200, results); // ✔️ 1)
console.log(didItWork); // ✔️
console.log(didItWork.age) // ❌
didItWork = {"age": 4} ; // ❌
}
// 注意雖然編譯報錯,但不影響最後js執行,Playground 運行結果
[LOG]: {
"age": 4
}
[LOG]: 4
可能有人困惑既然void類型無法進行其他操作,為什麼要允許1)處賦值void類型給變數。TypeScript 1.4語言規範 給出了如下說明:
注意:我們考慮過禁止聲明Void類型的變數,因為它們沒有用處。 但是,由於允許將Void作為泛型類型或函數的類型參數,因此不允許Void屬性或參數是不可行的。
function foo<T>(param:T) {
let localParam:T = param;
}
foo<number>(3); // ✔️
foo<void>(undefined); // ✔️
這麼處理的合理性在於,回調函數的調用者通過聲明callback 返回void, 它最清楚也可以保證返回值不會被使用。