TypeScript學習文件——高階篇
- TypeScript學習高階篇第一章:變數宣告
- TypeScript學習高階篇第二章:型別推斷
- TypeScript學習高階篇第三章:列舉
-
TypeScript學習高階篇第四章:公共型別
- 4.1
Partial<Type>
- 4.2
Required<Type>
- 4.3
Readonly<Type>
- 4.4
Record<Keys,Type>
- 4.5
Pick<Type, Keys>
- 4.6
Omit<Type, Keys>
- 4.7
Exclude<Type, ExcludedUnion>
- 4.8
Extract<Type, Union>
- 4.9
NonNullable
- 4.10
Parameters<Function Type>
- 4.11
ConstructorParameters
- 4.12
ReturnType
- 4.13 InstanceType
- 4.14 ThisParameterType
- 4.15 OmitThisParameter
- 4.16 ThisType
- 4.17 字串操作型別
- 4.1
- TypeScript學習高階篇第五章:Symbols
- TypeScript學習高階篇第六章:型別相容性
- TypeScript學習高階篇第七章:迭代器和生成
TypeScript學習高階篇第一章:變數宣告
let
和const
是JavaScript中變數宣告的兩個相對較新的概念。正如我們前面提到的, let
在某些方面與 var
相似,但允許使用者避免在JavaScript中遇到的一些常見的 "麻煩"。
const
是let
的一個擴充套件,它可以防止重新賦值給一個變數。
由於TypeScript是JavaScript的擴充套件,該語言自然支援 let
和 const
。在這裡,我們將進一步闡述這些新的宣告,以及為什麼它們比 var
更適合。
如果你已經不經意地使用了JavaScript,那麼下一節可能是重新整理你記憶的一個好方法。如果你對JavaScript中 var
宣告的所有怪癖非常熟悉,你可能會發現跳過前面會更容易。
1.1 var變數宣告
在JS中宣告一個變數,傳統上都是用var
關鍵字來完成。
var a = 10
正如你可能已經發現的,我們剛剛聲明瞭一個名為a
的變數,其值為10
。
我們也可以在一個函式中宣告一個變數:
function f() {
var message = "Hello, world!";
return message;
}
而我們也可以在其他函式中訪問這些相同的變數:
function f() {
var a = 10;
return function g() {
var b = a + 1;
return b;
};
}
var g = f();
g(); // returns '11'
在上面這個例子中, g 捕獲了 f 中宣告的變數 a 。在 g 被呼叫的任何時候, a 的值都將與 f 中 a 的值相聯絡。
function f() {
var a = 1;
a = 2;
var b = g();
a = 3;
return b;
function g() {
return a;
}
}
f(); // returns '2'
1.2 作用域法則
對於那些習慣於其他語言的人來說, var
宣告有一些奇怪的作用域範圍規則。以下面的例子為例:
function f(shouldInitialize: boolean) {
if (shouldInitialize) {
var x = 10;
}
return x;
}
f(true); // 返回 '10'
f(false); // 返回 'undefined'
有些讀者可能會對這個例子產生懷疑。變數 x 是在 if
塊中宣告的,但我們卻能從該塊之外訪問它。這是因為 var
宣告可以在其包含的函式、模組、名稱空間或全域性範圍內的任何地方訪問(所有這些我們將在後面討論),而不考慮包含的塊。有些人把這稱為 var
作用域或函式作用域。引數也是函式作用域。
這些作用域規則會導致幾種型別的錯誤。它們加劇的一個問題是,多次宣告同一個變數並不是一個錯誤。
function sumMatrix(matrix: number[][]) {
var sum = 0;
for (var i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (var i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
也許對於一些有經驗的JavaScript開發者來說,這很容易被發現,但是內部 for-loop
會意外地覆蓋變數 i
,因為 i
指的是同一個函式範圍的變數。正如有經驗的開發者現在所知道的,類似的各種bug會在程式碼審查中溜走,並會成為無盡的挫折來源。
1.3 變數捕獲的怪癖
花點時間猜一猜下面這段話的輸出是什麼:
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 100 * i);
}
對於那些不熟悉的人來說, setTimeout
將嘗試在一定數量的毫秒後執行一個函式(儘管要等待其他東西停止執行)。最後的結果是十行10。
許多JavaScript開發人員對這種行為非常熟悉,但如果你感到驚訝,你肯定不是一個人。大多數人都希望輸出的結果是:1 2 3 4 5 6 7 8 9 10。
還記得我們前面提到的關於變數捕獲的問題嗎?我們傳遞給 setTimeout
的每個函式表示式實際上都是指同一範圍內的同一個 i
。
讓我們花點時間考慮一下這意味著什麼。setTimeout
將在若干毫秒之後執行一個函式,但只有在 for迴圈停止執行之後;當 for 迴圈停止執行時, i 的值是 10 。因此,每次給定的函式被呼叫時,它將打 印出 10 !
一個常見的解決方法是使用IIFE
--一個立即呼叫的函式表示式--來捕獲每次迭代的 i 。
for (var i = 0; i < 10; i++) {
// 通過呼叫一個帶有其當前值的函式
// 捕捉'i'的當前狀態
(function (i) {
setTimeout(function () {
console.log(i);
}, 100 * i);
})(i);
}
這種看起來很奇怪的模式其實是很常見的。引數列表中的 i 實際上是對 for 迴圈中宣告的 i 的影子,但由於我們對它們的命名相同,所以我們不必對迴圈體進行過多的修改。
1.4 let變數宣告
現在你已經發現 var
有一些問題,這正是 let
語句被引入的原因。除了使用的關鍵字外, let 語句的寫法與 var 語句相同。
let hello = 'hello'
關鍵的區別不在語法上,而在語義上,我們現在要深入研究
1.5 塊級作用域
當一個變數使用 let
宣告時,它使用了一些人所說的詞法範圍或塊法範圍。與用 var
宣告的變數不同, block-scope
塊級作用域變數的作用域會洩露給其包含的函式, 而在其最近的包含塊或 for-loop
之外是不可見的。
function f(input: boolean) {
let a = 100;
if (input) {
// 引用'a'仍然可以
let b = a + 1;
return b;
}
// 錯誤:這裡不存在'b'。
return b;
}
在這裡,我們有兩個區域性變數 a 和 b 。a 的作用域僅限於 f 的主體,而 b 的作用域僅限於包含 if 語句的塊。
在 catch
子句中宣告的變數也有類似的作用域規則
try {
throw "oh no!";
} catch (e) {
console.log("Oh well.");
}
// Error: 這裡不存在'e'。
console.log(e);
塊級作用域變數的另一個屬性是,在它們被實際宣告之前,它們不能被讀或寫到。雖然這些變數在它們的整個作用域中都是 "存在 "的,但是直到它們被宣告之前的所有點都是它們的時間死角的一部分。這只是一種複雜的說法,你不能在 let
語句之前訪問它們,幸運的是TypeScript會讓你知道這一點。
a++; // 在宣告之前使用'a'是非法的。
let a;
需要注意的是,你仍然可以在宣告之前捕獲一個塊範圍的變數。唯一的問題是,在宣告之前呼叫該函式是非法的。如果以ES2015為目標,現代執行時將丟擲一個錯誤;然而,現在TypeScript是允許的,不會將此作為一個錯誤報告。
function foo() {
// 可以捕捉到 "a"。
return a;
}
// 在宣告'a'之前非法呼叫'foo'。
// runtimes應該在這裡丟擲一個錯誤
foo();
let a;
1.6 重複宣告和投影
對於var
宣告,我們提到,你聲明瞭多少次變數並不重要,你只是得到了一個。
function f(x) {
var x; var x;
if (true) {
var x;
}
}
在上面的例子中,所有關於 x 的宣告實際上指的是同一個 x ,這是完全有效的。這往往會成為錯誤的根源。值得慶幸的是, let
的宣告並不那麼寬容。
let x = 10;
let x = 20; // 錯誤:不能在同一範圍內重新宣告'x'。
變數不一定要都是塊範圍的,TypeScript才會告訴我們有一個問題。
function f(x) {
let x = 100; // 錯誤:干擾了引數宣告
}
function g() {
let x = 100;
var x = 100; // 錯誤:不能同時有'x'的宣告
}
這並不是說一個塊作用域變數永遠不能和一個函式作用域變數一起宣告。區塊作用域變數只是需要在一個明顯不同的區塊中宣告。
function f(condition, x) {
if (condition) {
let x = 100;
return x;
}
return x;
}
f(false, 0); // 返回 0
f(true, 0); // 返回 100
在一個更加巢狀的作用域中引入一個新名字的行為被稱為投影。這是一把雙刃劍,因為它可以在意外影射的情況下自行引入某些錯誤,同時也可以防止某些錯誤。例如,想象一下我們之前用 let 變數編寫的sumMatrix
函式:
function sumMatrix(matrix: number[][]) {
let sum = 0;
for (let i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (let i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
這個版本的迴圈實際上會正確地執行求和,因為內迴圈的 i 會對外迴圈的 i 產生陰影。
為了寫出更清晰的程式碼,通常應避免使用投影。雖然在某些情況下,利用它可能是合適的,但你應該使用你的最佳判斷。
1.7 塊級作用域變數捕獲
當我們第一次觸及用 var
宣告捕獲變數的想法時,我們簡要地討論了變數一旦被捕獲是如何行動的。為了給大家一個更好的直觀印象,每次執行一個作用域時,它都會建立一個變數的 "環境"。這個環境和它捕獲的變數甚至在它的作用域內的所有東西都執行完畢後仍然存在。
function theCityThatAlwaysSleeps() {
let getCity;
if (true) {
let city = "Seattle";
getCity = function () {
return city;
};
}
return getCity();
}
因為我們已經從它的環境中捕獲了 city
,所以儘管 if
塊已經執行完畢,我們仍然能夠訪問它。
回想一下,在我們之前的 setTimeout
例子中,我們最終需要使用IIFE
來捕獲 for 迴圈的每個迭代中的變數狀態。實際上,我們所做的是為我們捕獲的變數建立一個新的變數環境。這有點麻煩,但幸運的是,在TypeScript中你再也不用這麼做了。
當宣告為迴圈的一部分時, let 宣告的行為有很大的不同。這些宣告並不只是給迴圈本身引入一個新的環境,而是在每個迭代中建立一個新的範圍。因為這就是我們在IIFE中所做的事情,我們可以改變我們以前的 setTimeout
的例子,只使用 let
宣告。
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 100 * i);
}
和預期一樣會列印:0 1 2 3 4 5 6 7 8 9
1.8 const
宣告
const
宣告是宣告變數的另一種方式
const numLivesForCat = 9;
它們就像 let 宣告一樣,但正如它們的名字所暗示的,一旦它們被繫結,它們的值就不能被改變。換句話說,它們有和 let 一樣的範圍規則,但你不能重新賦值給它們。
這不應該與它們所指的值是不可改變的想法相混淆。
const numLivesForCat = 9;
const kitty = {
name: "Aurora",
numLives: numLivesForCat,
};
// 錯誤
kitty = {
name: "Danielle",
numLives: numLivesForCat,
};
// 以下都正確
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
除非你採取特定的措施來避免它,否則常量變數的內部狀態仍然是可以修改的。幸運的是,TypeScript允許你指定一個物件的成員是 readonly
的。
1.9 let
與const
比較
鑑於我們有兩種具有類似範圍語義的宣告,我們很自然地會問自己應該使用哪一種。像大多數廣泛的問題一樣,答案是:這取決於。
根據最小特權原則,除了那些你打算修改的宣告外,所有的宣告都應該使用 const。其理由是,如果一個變數不需要被寫入,那麼在同一個程式碼庫中工作的其他人就不應該自動能夠寫入該物件,他們需要考慮是否真的需要重新賦值給該變數。在推理資料流時,使用 const 也會使程式碼更可預測。
使用你的最佳判斷,如果適用的話,請與你的團隊其他成員協商此事。
下面文件大部分內容都使用 let
宣告。
1.10 解構
解構賦值語法是一種 Javascript表示式。通過解構賦值, 可以將屬性/值從物件/陣列中取出,賦值給其他變數。
1.11 陣列析構
最簡單的解構形式是陣列解構賦值。
let input = [1, 2];
let [first, second] = input;
console.log(first); // 輸出 1
console.log(second); // 輸出 2
這將建立兩個新的變數,命名為 first 和 second 。這等同於使用索引,但要方便得多。
first = input[0];
second = input[1];
解構也適用於已經宣告的變數。
// 交換變數
[first, second] = [second, first];
而且是帶引數的函式:
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f([1, 2]);
你可以使用語法 ...
為列表中的剩餘專案建立一個變數
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 輸出 1
console.log(rest); // 輸出 [ 2, 3, 4 ]
當然,由於這是JavaScript,你可以直接忽略你不關心的拖尾元素:
let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1
1.12 元組解構
元組可以像陣列一樣被去結構化;去結構化的變數得到相應元組元素的型別:
let tuple: [number, string, boolean] = [7, "hello", true];
let [a, b, c] = tuple; // a: number, b: string, c: boolean
對一個元組進行解構,超出其元素的範圍是一個錯誤:
let [a, b, c, d] = tuple; // 錯誤,索引3處沒有元素
和陣列一樣,你可以用 ...
對元組的其餘部分進行解構,以得到一個更短的元組:
let [a, ...bc] = tuple; // bc: [string, boolean]
let [a, b, c, ...d] = tuple; // d: [], 空 tuple
或者忽略尾部元素,或者忽略其他元素:
let [a] = tuple; // a: number
let [, b] = tuple; // b: string
1.13 物件解構
你也可以做物件的解構:
let o = { a: "foo", b: 12, c: "bar",};
let { a, b } = o;
這就從 o.a
和 o.b
中建立了新的變數 a
和 b
。注意,如果你不需要 c
,你可以跳過它。 就像陣列去結構化一樣,你可以不用宣告就進行賦值:
({ a, b } = { a: "baz", b: 101 });
請注意,我們必須用圓括號包圍這個語句。JavaScript通常將{作為塊的開始來解析。
你可以使用語法 ...
為物件中的剩餘專案建立一個變數:
let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;
- 屬性重新命名
你也可以給屬性起不同的名字:
let { a: newName1, b: newName2 } = o;
這裡的語法開始變得混亂了。你可以把 a: newName1
讀作 "a as newName1"
。方向是從左到右,就像你寫的一樣:
let newName1 = o.a;
let newName2 = o.b;
令人困惑的是,這裡的冒號並不表示型別。如果你指定了型別,仍然需要寫在整個結構解構之後。
let { a, b }: { a: string; b: number } = o;
- 預設值
預設值讓你指定一個預設值,以防一個屬性未被定義:
function keepWholeObject(wholeObject: { a: string; b?: number }) {
let { a, b = 1001 } = wholeObject;
}
在這個例子中, b?
表示 b
是可選的,所以它可能是未定義的。 keepWholeObject
現在有一個 wholeObject
的變數,以及屬性 a
和 b
,即使 b
是未定義的。
1.14 Function宣告
去結構化在函式宣告中也起作用。對於簡單的情況,這是很直接的。
type C = { a: string; b?: number };
function f({ a, b }: C): void {
// ...
}
但是對於引數來說,指定預設值是比較常見的,而用解構的方式來獲得預設值是很棘手的。首先,你需要記住把模式放在預設值之前。
function f({ a = "", b = 0 } = {}): void {
// ...
}
f();
然後,你需要記住在 destructured
屬性上給可選屬性一個預設值,而不是主初始化器。記住, C的定義是b可選的。
function f({ a, b = 0 } = { a: "" }): void {
// ...
}
f({ a: "yes" }); // 正確,b = 0
f(); // 正確, 預設 { a: "" }, 然後預設為 b = 0
f({}); // 錯誤,如果你提供一個引數,'a'是必須的
小心使用解構。正如前面的例子所展示的,除了最簡單的析構表示式之外,任何東西都會令人困惑。這在深度巢狀的結構化中尤其如此,即使不堆積重新命名、預設值和型別註釋,也會變得非常難以理解。儘量保持結構化表示式的小而簡單。你總是可以自己寫出解構會產生的賦值。
1.15 展開
展開操作符與解構相反。它允許你將一個數組分散到另一個數組中,或者將一個物件分散到另一個物件中。比如說:
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
這使 bothPlus
的值為 [0, 1, 2, 3, 4, 5]
。展開建立first
和second
的淺層拷貝。它們不會因 為展開而改變。
你也可以展開物件。
let defaults = {
food: "spicy",
price: "$$",
ambiance: "noisy"
};
let search = {
...defaults,
food: "rich"
};
現在的 search 是 { food: "rich", price: "$$", ambiance: "noisy" }
。物件展開比陣列展開更復雜。像陣列展開一樣,它從左到右進行,但結果仍然是一個物件。這意味著展開物件中較晚出現的屬性會覆蓋較早出現的屬性。因此,如果我們修改前面的例子,在最後展開:
let defaults = {
food: "spicy",
price: "$$",
ambiance: "noisy"
};
let search = {
food: "rich",
...defaults
};
然後, defaults 中的食物屬性覆蓋了 food: "rich"
,這不是我們在這種情況下想要的。
物件傳播也有其他一些令人驚訝的限制。首先,它只包括一個物件自己的、可列舉的屬性。基本上,這意味著當你傳播一個物件的例項時,你會失去方法。
class C {
p = 12;
m() {}
}
let c = new C();
let clone = {
...c
};
clone.p; // 正確
clone.m(); // 錯誤!
TypeScript編譯器不允許從通用函式中展開型別引數。該功能預計將在未來的語言版本中出現。
TypeScript學習高階篇第二章:型別推斷
在TS中,有幾個地方在沒有顯式型別註釋的情況下,使用型別推理來提供型別資訊。例如,在這段程式碼中:
//let x: number
let x = 3
x
變數的型別被推斷為 number
。這種推斷髮生在初始化變數和成員、設定引數預設值和確定函式返回型別時。
在大多數情況下,型別推斷是直截了當的。在下面的章節中,我們將探討型別推斷的一些細微差別。
2.1 最佳公共型別
當從幾個表示式中進行型別推斷時,這些表示式的型別被用來計算一個 "最佳公共型別"。比如說:
// let x: (number | null)[]
let x = [0, 1, null];
為了推斷上面例子中 x
的型別,我們必須考慮每個陣列元素的型別。這裡我們得到了兩個陣列型別的選擇: number
和 null
。最佳公共型別演算法考慮了每個候選型別,並選擇了與所有其他候選型別相容的型別。
因為最佳公共型別必須從所提供的候選型別中選擇,所以在某些情況下,型別有共同的結構,但沒有一個型別是所有候選型別的超級型別。比如說:
// let zoo: (Rhino | Elephant | Snake)[]
let zoo = [new Rhino(), new Elephant(), new Snake()];
理想情況下,我們可能希望 zoo
被推斷為 Animal[]
,但是因為陣列中沒有嚴格意義上的 Animal
型別的物件,所以我們沒有對陣列元素型別進行推斷。為了糾正這一點,當沒有一個型別是所有其他候選型別的超級型別時,就明確地提供型別。
// let zoo: Animal[]t
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
當沒有找到最好的共同型別時,產生的推論是聯合陣列型別, (Rhino | Elephant | Snake)[]
。
2.2 上下文型別
在TS的某些情況下,型別推理也在"另一個方向"發揮作用。這被稱為”上下文型別化“。當表示式的型別被他的位置所暗示時,上下文型別就發生了。例如:
window.onmousedown = function (mouseEvent) {
console.log(mouseEvent.button);
console.log(mouseEvent.kangaroo); // Ⓧ 在'MouseEvent'型別上不存在'kangaroo'屬性。
};
在這裡,TypeScript 型別檢查器使用 window.onmousedown
函式的型別來推斷賦值右側的函式表示式的型別。當它這樣做時,它能夠推斷出 mouseEvent
引數的型別,它確實包含一個按鈕button屬性,但不包含袋鼠kangaroo屬性。
這樣做的原因是 window
已經在其型別中聲明瞭 onmousedown
。
// 宣告有一個名為'window'的全域性變數
declare var window: Window & typeof globalThis;
// 這被宣告為(簡化版)。
interface Window extends GlobalEventHandlers {
// ...
}
// 其中定義了很多已知的處理程式事件
interface GlobalEventHandlers {
onmousedown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
// ...
}
TypeScript足夠聰明,在其他情況下也能推斷出型別:
window.onscroll = function (uiEvent) {
// Ⓧ 屬性 "button" 不存在於 "Event"型別上。
console.log(uiEvent.button);
};
基於上述函式被分配給 Window.onscroll
的事實,TypeScript知道 uiEvent
是一個 UIEvent
,而不是像前面的例子那樣是 MouseEvent
。 UIEvent
物件不包含按鈕屬性,所以TypeScript會丟擲一個錯誤。
如果這個函式不在上下文型別的位置,這個函式的引數將隱含有型別 any
,並且不會發出錯誤(除非你使用 noImplicitAny
選項)。
const handler = function (uiEvent) {
console.log(uiEvent.button); // <- 正確
};
我們也可以明確地給函式的引數提供型別資訊,以覆蓋任何上下文的型別。
window.onscroll = function (uiEvent: any) {
console.log(uiEvent.button); // <- 現在也沒有錯誤
};
然而,這段程式碼將記錄 undefined
的內容,因為 uiEvent 沒有名為按鈕的屬性。
上下文型別化在很多情況下都適用。常見的情況包括函式呼叫的引數、賦值的右側、型別斷言、物件和陣列字面量的成員,以及返回語句。上下文型別也作為最佳普通型別的候選型別。比如說:
function createZoo(): Animal[] {
return [new Rhino(), new Elephant(), new Snake()];
}
在這個例子中,最佳普通型別有一組四個候選者。 Animal
, Rhino
, Elephant
和 Snake
。其中,Animal
可以被最佳共同型別演算法所選擇。
TypeScript學習高階篇第三章:列舉
Enums
是TypeScript的少數功能之一,它不是JavaScript的型別級擴充套件。
列舉允許開發者定義一組命名的常量。使用列舉可以使其更容易記錄意圖,或建立一組不同的情況。TypeScript提供了基於數字和字串的列舉。
3.1 數值型列舉
我們首先從數字列舉開始,如果你來自自其他語言,可能會更熟悉它。一個列舉可以用 enum關鍵字來定義。
enum Direction {
Up = 1,
Down,
Left,
Right,
}
上面,我們有一個數字列舉,其中 Up
被初始化為 1 ,所有下面的成員從這一點開始自動遞增。換句話說, Direction.Up
的值是 1 , Down
是 2 , Left
是 3 , Right
是 4 。
如果我們願意,我們可以完全不使用初始化器:
enum Direction {
Up,
Down,
Left,
Right,
}
這裡,Up的值是0,Down是1,依次類推。這種自動遞增的行為對於我們可能不關心成員值本身,但關心每個值與同一列舉中的其他值不同的情況很有用。
使用列舉很簡單:只需將任何成員作為列舉本身的一個屬性來訪問,並使用列舉的名稱來宣告型別:
enum UserResponse {
No = 0,
Yes = 1,
}
function respond(recipient: string, message: UserResponse): void {
// ...
}
respond("Princess Caroline", UserResponse.Yes);
數字列舉可以混合在計算和常量成員中(見下文)。簡而言之,沒有初始化器的列舉要麼需要放在第一位,要麼必須放在用數字常量或其他常量列舉成員初始化的數字列舉之後。換句話說,下面的情況是不允許的:
enum E {
A = getSomeValue(),
B,
// Ⓧ Enum成員必須有初始化器。
}
3.2 字串列舉
字串列舉是一個類似的概念,但有一些細微的執行時差異,如下文所述。在一個字串列舉中,每個成員都必須用一個字串字頭或另一個字串列舉成員進行常量初始化。
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
雖然字串列舉沒有自動遞增的行為,但字串列舉有一個好處,那就是它們可以很好地 "序列化"。換句話說,如果你在除錯時不得不讀取一個數字列舉的執行時值,這個值往往是不透明的--它本身並不傳達任何有用的意義(反向對映往往可以),字串列舉允許你在程式碼執行時給出一個有意義的、可讀的值,與列舉成員本身的名稱無關。
3.3 異構列舉
從技術上講,列舉可以與字串和數字成員混合,但不清楚為什麼你會想這樣做:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
除非你真的想以一種巧妙的方式利用JavaScript的執行時行為,否則建議你不要這樣做。
3.4 計算型和常量型成員
每個列舉成員都有一個與之相關的值,可以是常量,也可以是計算值。一個列舉成員被認為是常數,如果:
- 它是列舉中的第一個成員,它沒有初始化器,在這種情況下,它被賦值為
0
:
// E.X is constant:
enum E { X,}
- 它沒有一個初始化器,而且前面的列舉成員是一個數字常數。在這種情況下,當前列舉成員的值將是前一個列舉成員的值加 1 :
// 'E1'和'E2'中的所有列舉成員都是常數。
enum E1 { X, Y, Z,}
enum E2 { A = 1, B, C,}
列舉成員用一個常量列舉表示式進行初始化。常量列舉表示式是TypeScript表示式的一個子集,可以在編譯時進行完全評估。一個表示式是一個常量列舉表示式,如果它是:
- 列舉表示式的字面意思(基本上是一個字串字面量或一個數字字面量);
- 對先前定義的常量列舉成員的引用(可以來自不同的列舉);
- 一個括號內的常量列舉表示式;
- 應用於常量列舉表示式的
+
,-
,~
單項運算子之一 ; -
+
,-
,*
,/
,%
,<<
,>>
,&
,|
,^
以常量列舉表示式為運算元的二元運算子。
如果常量列舉表示式被評估為 NaN
或 Infinity
,這是一個編譯時錯誤。
在所有其他情況下,列舉成員被認為是計算出來的。
enum FileAccess {
// 常量成員
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// 計算成員
G = "123".length,
}
3.5 聯合列舉和列舉成員型別
有一個特殊的常量列舉成員的子集沒有被計算:字面列舉成員。字面列舉成員是一個沒有初始化值的常量列舉成員,或者其值被初始化為:
- 任何字串(例如:
"foo"
,"bar"
,"baz"
) - 任何數字字頭(例如: 1 , 100)
- 應用於任何數字字面的單數減號(例如: -1 , -100 )
當一個列舉中的所有成員都有列舉的字面價值時,一些特殊的語義就會發揮作用。
首先,列舉成員也成為了型別。例如,我們可以說某些成員只能有一個列舉成員的值:
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square,
// Ⓧ 型別 'ShapeKind.Square' 不能被分配給型別 'ShapeKind.Circle'
radius: 100,
}
另一個變化是列舉型別本身有效地成為每個列舉成員的聯盟。通過聯合列舉,型別系統能夠利用這一事實,即它知道存在於列舉本身的精確的值集。正因為如此,TypeScript可以捕捉到我們可能錯誤地比較數值的錯誤。比如說:
enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
// Ⓧ 這個條件將總是返回'true',因為'E.Foo'和'E.Bar'的型別沒有重合。
//...
}
}
在這個例子中,我們首先檢查了 x
是否不是 E.Foo
。如果這個檢查成功了,那麼我們的 ||
就會短 路, if
語句的主體就會執行。然而,如果檢查沒有成功,那麼 x 就只能是 E.Foo
,所以看它是否等於 E.Bar
就沒有意義了。
3.6 執行時的列舉
列舉是在執行時存在的真實物件。例如,下面這個列舉
enum E {
X,
Y,
Z,
}
實際上可以被傳遞給函式:
enum E {
X,
Y,
Z,
}
function f(obj: { X: number }) {
return obj.X;
}
// 可以正常工作,因為'E'有一個名為'X'的屬性,是一個數字。
f(E);
3.7 編譯時的列舉
儘管Enum
是在執行時存在的真實物件, keyof
關鍵字的工作方式與你對物件的預期不同。相反,使用keyof
typeof
來獲得一個將所有Enum
鍵表示為字串的型別。
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
/**
* 這相當於:
* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
*/
type LogLevelStrings = keyof typeof LogLevel;
function printImportant(key: LogLevelStrings, message: string) {
const num = LogLevel[key];
if (num <= LogLevel.WARN) {
console.log("Log level key is:", key);
console.log("Log level value is:", num);
console.log("Log level message is:", message);
}
}
printImportant("ERROR", "This is a message");
- 反向對映
除了為成員建立一個帶有屬性名稱的物件外,數字列舉的成員還可以得到從列舉值到列舉名稱的反向對映。例如,在這個例子中:
enum Enum {
A,
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
TypeScript將其編譯為以下的JavaScript:
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
在這段生成的程式碼中,一個列舉被編譯成一個物件,它同時儲存了正向 ( name -> value )
和反向 ( value -> name )
的對映關係。對其他列舉成員的引用總是以屬性訪問的方式發出,而且從不內聯。
請記住,字串列舉成員根本不會被生成反向對映。
-
const
列舉
在大多數情況下,列舉是一個完全有效的解決方案。然而有時要求比較嚴格。為了避免在訪問列舉值時支付額外的生成程式碼和額外的間接性的代價,可以使用 const
列舉。常量列舉是使用我們列舉上的 const
修飾符來定義的。
const enum Enum {
A = 1,
B = A * 2,
}
常量列舉只能使用常量列舉表示式,與普通列舉不同,它們在編譯過程中被完全刪除。常量列舉成員在使用地點被內聯。這是可能的,因為常量列舉不能有計算的成員。
const enum Direction {
Up,
Down,
Left,
Right,
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
在生成的程式碼中,將變成:
"use strict";
let directions = [
0 /* Up */ ,
1 /* Down */ ,
2 /* Left */ ,
3 /* Right */ ,
];
3.8 環境列舉
環境列舉是用來描述已經存在的列舉型別的形狀。
declare enum Enum {
A = 1,
B,
C = 2,
}
環境列舉和非環境列舉之間的一個重要區別是,在常規列舉中,如果其前面的列舉成員被認為是常量,那麼沒有初始化器的成員將被認為是常量。相反,一個沒有初始化器的環境(和非常量)列舉成員總是被認為是計算的。
3.9 物件與列舉
在現代TypeScript中,你可能不需要一個列舉,因為一個物件的常量就足夠了:
const enum EDirection {
Up,
Down,
Left,
Right,
}
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
// (enum member) EDirection.Up = 0
EDirection.Up;
// (property) Up: 0
ODirection.Up;
// 將列舉作為一個引數
function walk(dir: EDirection) {}
// 它需要一個額外的行來拉出數值
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
walk(EDirection.Left);
run(ODirection.Right);
與TypeScript的列舉相比,支援這種格式的最大理由是,它使你的程式碼庫與JavaScript的狀態保持一致, when/if
列舉被新增到JavaScript中,那麼你可以轉移到額外的語法。
TypeScript學習高階篇第四章:公共型別
TypeScript 提供了幾個實用型別,以促進常見的型別轉換。這些實用程式在全域性範圍內可用。“
4.1 Partial<Type>
構建一個型別,將 Type
的所有屬性設定為可選。這個工具將返回一個表示給定型別的所有子集的型別。
例子:
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1:Todo = {
title: "organize desk",
description: "clear clutter",
};
//因為可以將所有屬性設定為可選,可選如果不設定即為undefined,所以拿此來測試
const todo2:Partial<Todo> = updateTodo(todo1, {
description: undefined,
})
4.2 Required<Type>
構建一個由 Type
的所有屬性組成的型別,設定為必填。與 Partial
相反:
interface Props {
a?: number;
b?: string;
}
const obj: Props = { a: 5 };
// error,型別 "{ a: number; }" 中缺少屬性 "b",但型別 "Required<Props>" 中需要該屬性
const obj2: Required<Props> = { a: 5 };
4.3 Readonly<Type>
構建一個型別, Type
的所有屬性設定為 readonly
,這意味著構建的型別的屬性不能被重新設定值。
interface Todo {
title: string;
}
const todo: Readonly<Todo> = {
title: "Delete inactive users",
};
// error
todo.title = "Hello";
這個工具對於表示將在執行時失敗的賦值表示式很有用(即當試圖重新分配一個凍結物件的屬性時)。
function freeze<Type>(obj: Type): Readonly<Type>;
4.4 Record<Keys,Type>
構建一個物件型別,其屬性鍵是 Keys ,其屬性值是 Type 。這個工具可以用來將一個型別的屬性對映到另一個型別:
interface CatInfo {
age: number;
breed: string;
}
type CatName = "miffy" | "boris" | "mordred";
const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
};
// const cats: Record<CatName, CatInfo>
console.log(cats.boris) // { age: 5, breed: 'Maine Coon' }
4.5 Pick<Type, Keys>
通過從 Type 中選取屬性集合 Keys (屬性名或屬性名的聯合)來構造一個型別:
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
// const todo: TodoPreview
todo;
4.6 Omit<Type, Keys>
通過從 Type
中選取所有屬性,然後刪除Keys
(屬性名或屬性名的聯合)來構造一個型別。
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: number;
}
type TodoPreview = Omit<Todo, "description">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
createdAt: 1615544252770,
};
// const todo: TodoPreview
todo;
type TodoInfo = Omit<Todo, "completed" | "createdAt">;
const todoInfo: TodoInfo = {
title: "Pick up kids",
description: "Kindergarten closes at 5pm",
};
// const todoInfo: TodoInfo
todoInfo;
4.7 Exclude<Type, ExcludedUnion>
通過從 Type
中排除所有可分配給 ExcludedUnion
的聯盟成員來構造一個型別。
// type T0 = "b" | "c"
type T0 = Exclude<"a" | "b" | "c", "a">;
// type T1 = "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
// type T2 = string | number
type T2 = Exclude<string | number | (() => void), Function>;
4.8 Extract<Type, Union>
通過從 Type
中提取可分配給 Union
的所有 union
成員,構造一個型別。
// type T0 = "a"
type T0 = Extract<"a" | "b" | "c", "a" | "f">
// type T1 = () => void
type T1 = Extract<string | number | (() => void), Function>
4.9 NonNullable
通過從 Type 中排除 null 和 undefined 來構造一個型別。
// type T0 = string | number
type T0 = NonNullable<string | number | undefined>;
// type T1 = string[]
type T1 = NonNullable<string[] | null | undefined>;
4.10 Parameters<Function Type>
從一個函式型別 Type
的引數中使用的型別構建一個元組型別。
declare function f1(arg: { a: number; b: string }): void;
// type T0 = []
type T0 = Parameters<() => string>;
// type T1 = [s: string]
type T1 = Parameters<(s: string) => void>;
// type T2 = [arg: unknown]
type T2 = Parameters<<T>(arg: T) => T>;
/*
type T3 = [arg: {
a: number;
b: string;
}]
*/
type T3 = Parameters<typeof f1>;
// type T4 = unknown[]
type T4 = Parameters<any>;
// type T5 = never
type T5 = Parameters<never>;
// type T6 = never
type T6 = Parameters<string>;
// type T7 = never
type T7 = Parameters<Function>;
4.11 ConstructorParameters
從建構函式的型別中構造一個元組或陣列型別。它產生一個具有所有引數型別的元組型別(如果 Type
不是一個函式,則為 never
型別)。
// type T0 = [message?: string]
type T0 = ConstructorParameters<ErrorConstructor>;
// type T1 = string[]
type T1 = ConstructorParameters<FunctionConstructor>;
// type T2 = [pattern: string | RegExp, flags?: string]
type T2 = ConstructorParameters<RegExpConstructor>;
// type T3 = unknown[]
type T3 = ConstructorParameters<any>;
// type T4 = never
type T4 = ConstructorParameters<Function>;
4.12 ReturnType
構建一個由函式 Type
的返回型別組成的型別。如果是泛型則是unknown
。
declare function f1(): { a: number; b: string };
// type T0 = string
type T0 = ReturnType<() => string>;
// type T1 = void
type T1 = ReturnType<(s: string) => void>;
// type T2 = unknown
type T2 = ReturnType<<T>() => T>;
// type T3 = number[]
type T3 = ReturnType<<T extends U, U extends number[]>() => T>;
/*
type T4 = {
a: number;
b: string;
}
*/
type T4 = ReturnType<typeof f1>;
// type T5 = any
type T5 = ReturnType<any>;
// type T6 = never
type T6 = ReturnType<never>;
// type T7 = any 報錯
type T7 = ReturnType<string>;
// type T8 = any 報錯
type T8 = ReturnType<Function>
4.13 InstanceType
構建一個由 Type
中建構函式的例項型別組成的型別。
class C {
x = 0;
y = 0;
}
// type T0 = C
type T0 = InstanceType<typeof C>;
// type T1 = any
type T1 = InstanceType<any>;
// type T2 = never
type T2 = InstanceType<never>;
// type T3 = any
type T3 = InstanceType<string>;
// type T4 = any
type T4 = InstanceType<Function>;
4.14 ThisParameterType
提取一個函式型別的 this
引數的型別,如果該函式型別沒有 this
引數,則為 unknown
。
function toHex(this: Number) {
return this.toString(16);
}
// n: number
function numberToString(n: ThisParameterType<typeof toHex>) {
return toHex.apply(n);
}
4.15 OmitThisParameter
移除 Type
的 this
引數。如果 Type
沒有明確宣告的 this
引數,結果只是 Type
。否則,一個沒有 this
引數的新函式型別將從 Type
建立。泛型被擦除,只有最後的過載簽名被傳播到新的函式型別。
function toHex(this: Number) {
return this.toString(16);
}
const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);
console.log(fiveToHex());
4.16 ThisType
這個工具並不返回一個轉換後的型別。相反,它作為一個上下文的 this
型別的標記。注意,必須啟用noImplicitThis
標誌才能使用這個工具。
ts型別中的&表示交叉型別, 主要用於組合現有的物件型別。
type ObjectDescriptor<D, M> = {
data?: D;
methods?: M & ThisType<D & M>; // 方法中的 'this' 型別是 D & M
};
function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
let data: object = desc.data || {};
let methods: object = desc.methods || {};
return { ...data, ...methods } as D & M;
}
let obj = makeObject({
data: { x: 0, y: 0 },
methods: {
moveBy(dx: number, dy: number) {
this.x += dx;
this.y += dy;
},
},
});
obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);
在上面的例子中,makeObject
的引數中的 methods
物件有一個包括 ThisType
的上下文型別,因此方法物件中 this
的型別是 { x: number, y: number } & { moveBy(dx: number, dy: number): number }
。注意 methods 屬性的型別如何同時是推理目標和方法中 this 型別的來源。 ThisType 標記介面只是在 lib.d.ts 中宣告的一個空介面。除了在物件字面的上下文型別中被識別之外,該介面的行為與任何空介面一樣。
4.17 字串操作型別
Uppercase<StringType>
Lowercase<StringType>
Capitalize<StringType>
Uncapitalize<StringType>
TypeScript包括一組型別,可以在型別系統中用於字串操作。你可以在 Template Literal Types 文件 中找到這些工具的用法。
TypeScript學習高階篇第五章:Symbols
從ECMAScript 2015(ES6)
開始, symbol
是一種原始的資料型別,就像 number
和 string
一樣。
symbol
值是通過呼叫Symbol
建構函式建立的。
let sym1 = Symbol();
let sym2 = Symbol("key"); // 可選的字串 key
Symbols 是不可改變的,而且是獨一無二的。
let sym2 = Symbol("key");
let sym3 = Symbol("key");
sym2 === sym3; // false, symbols 是唯一的
就像字串一樣,Symbols
可以被用作物件屬性的鍵。
const sym = Symbol();
let obj = {
[sym]: "value",
};
console.log(obj[sym]); // "value"
Symbols
也可以與計算屬性宣告結合起來,以宣告物件屬性和類成員。
const getClassNameSymbol = Symbol();
class C {
[getClassNameSymbol]() {
return "C";
}
}
let c = new C();
let className = c[getClassNameSymbol](); // "C"
5.1 unique symbol
為了能夠將 symbols
作為唯一的字面符號,提供了一個特殊的型別 unique symbol
。 unique symbol
是 symbol
的一個子型別,只在呼叫 Symbol()
或 Symbol.for()
或明確的型別註釋時產生。這種型別只允許在常量宣告和只讀靜態屬性中使用,為了引用一個特定的唯一符號,你必須使用typeof
操作符。每個對唯一符號的引用都意味著一個完全獨特的身份,它與一個給定的宣告相聯絡。
declare const sym1: unique symbol;
// sym2只能是一個常數參考。
let sym2: unique symbol = Symbol();
// Ⓧ 型別為 "唯一符號 "的變數必須是 "const"型別。
// 執行正確--指的是一個獨特的 symbol,但其身份與'sym1'相聯絡。
let sym3: typeof sym1 = sym1;
// 也是正確的
class C {
static readonly StaticSymbol: unique symbol = Symbol();
}
因為每個 unique symbol
都有一個完全獨立的身份,沒有兩個 unique symbol
型別是可以相互分配或比較的。
const sym2 = Symbol();
const sym3 = Symbol();
// 這個條件將總是返回'false',因為'typeof sym2'和'typeof sym3'的型別沒有重合。
if (sym2 === sym3) {
// ...
}
5.2 知名的 Symbols
除了使用者定義的 symbols
外,還有著名的內建 symbols
。內建符號被用來表示內部語言行為。
下面是一個著名的 symbols
列表:
5.2.1 Symbol.hasInstance
一個確定建構函式物件,是否識別一個物件為建構函式的例項之一的方法。由instanceof
操作符的語義呼叫。
5.2.2 Symbol.isConcatSpreadable
一個布林值,表示一個物件應該被Array.prototype.concat
平鋪到其陣列元素。
5.2.3 Symbol.iterator
返回一個物件的預設迭代器的方法。被 for-of
語句的語義所呼叫。
5.2.4 Symbol.match
一個正則表示式方法,與字串的正則表示式相匹配。由 String.prototype.match
方法呼叫。
5.2.5 Symbol.replace
一個正則表示式方法,用於替換一個字串中匹配的子串。由 String.prototype.replace
方法呼叫。
5.2.6 Symbol.search
一個正則表示式方法,返回字串中符合正則表示式的索引。由 String.prototype.search
方法呼叫。
5.2.7 Symbol.species
一個函式值的屬性,是用於建立派生物件的建構函式。
5.2.8 Symbol.split
一個正則表示式方法,在符合正則表示式的索引處分割一個字串。由 String.prototype.split
方法呼叫。
5.2.9 Symbol.toPrimitive
將一個物件轉換為一個相應的基元值的方法。由ToPrimitive
抽象操作呼叫。
5.2.10 Symbol.toStringTag
一個字串值,用於建立一個物件的預設字串描述。由內建方法 Object.prototype.toString
呼叫。
5.2.11 Symbol.unscopables
一個物件,其自身的屬性名是被排除在相關物件的 'with' 環境繫結之外的屬性名。
TypeScript學習高階篇第六章:型別相容性
TypeScript中的型別相容性是基於結構子型別的。結構分型是一種完全基於其成員的型別關係的方式。
這與名義型別不同。考慮一下下面的程式碼:
interface Pet {
name: string;
}
class Dog {
name: string;
}
let pet: Pet;
// 正確,因為結構化型別
pet = new Dog();
在像 C#
或 Java
這樣的名義型別語言中,相應的程式碼將是一個錯誤,因為 Dog
類沒有明確地描述自己是 Pet
介面的實現者。
TypeScript的結構型別系統是根據JavaScript程式碼的典型寫法設計的。因為JavaScript廣泛使用匿名物件,如函式表示式和物件字面量,用結構型別系統而不是命名型別系統來表示JavaScript庫中的各種關係要自然得多。
6.1 關於健全性的說明
TypeScript 的型別系統允許某些在編譯時無法知道的操作是安全的。當一個型別系統具有這種屬性時, 它被稱為不 "健全"。我們仔細考慮了 TypeScript 允許不健全行為的地方,在這篇文件中,我們將解釋這 些發生的地方以及它們背後的動機情景。
6.2 起步
TypeScript的結構型別系統的基本規則是,如果 y
至少有與 x
相同的成員,那麼 x
與 y
是相容的。wwww
interface Pet {
name: string;
}
let pet: Pet;
// dog's 推斷型別是 { name: string; owner: string; }
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
pet = dog
為了檢查 dog
是否可以被分配給 pet
,編譯器檢查 pet
的每個屬性,以找到 dog
中相應的相容屬 性。在這種情況下, dog
必須有一個名為 name
的成員,它是一個字串。它有,所以賦值是允許的。
在檢查函式呼叫引數時,也使用了同樣的賦值規則。
interface Pet {
name: string;
}
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
function greet(pet: Pet) {
console.log("Hello, " + pet.name);
}
greet(dog); // 正確
請注意, dog
有一個額外的 owner
屬性,但這並不產生錯誤。在檢查相容性時,只考慮目標型別(本例中為 Pet
)的成員。
這個比較過程是遞迴進行的,探索每個成員和子成員的型別。
6.3 對比兩個函式
雖然比較原始型別和物件型別是相對直接的,但什麼樣的函式應該被認為是相容的,這個問題就有點複雜了。讓我們從兩個函式的基本例子開始,這兩個函式只在引數列表上有所不同:
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // 正確
x = y; // 錯誤
為了檢查 x
是否可以分配給 y
,我們首先看一下引數列表。 x
中的每個引數在 y
中都必須有一個型別相容的對應引數。注意,引數的名稱不被考慮,只考慮它們的型別。在這種情況下, x
中的每個引數在y
中都有一個對應的相容引數,所以這個賦值是允許的。
第二個賦值是一個錯誤,因為 y
有一個 x
沒有的必要的第二個引數,所以這個賦值是不允許的。
你可能想知道為什麼我們允許像例子中的 y = x
那樣 "丟棄 "引數。這個賦值被允許的原因是,忽略額外的函式引數在JavaScript中其實很常見。例如, Array#forEach
為回撥函式提供了三個引數:陣列元素、其索引和包含陣列。儘管如此,提供一個只使用第一個引數的回撥是非常有用的:
let items = [1, 2, 3];
// 不要強迫這些額外引數
items.forEach((item, index, array) => console.log(item));
// 應該沒有問題!
items.forEach((item) => console.log(item));
現在讓我們看看如何處理返回型別,使用兩個只因返回型別不同的函式:
let x = () => ({ name: "Alice" });
let y = () => ({ name: "Alice", location: "Seattle" });
x = y; // 正確
y = x; // 錯誤,因為x()缺少一個location屬性
型別系統強制要求源函式的返回型別是目標型別的返回型別的一個子型別。
6.4 函式引數的雙差性
enum EventType {
Mouse,
Keyboard,
}
interface Event {
timestamp: number;
}
interface MyMouseEvent extends Event {
x: number;
y: number;
}
interface MyKeyEvent extends Event {
keyCode: number;
}
function listenEvent(eventType: EventType, handler: (n: Event) => void) {
/* ... */
}
// 不健全,但有用且常見
listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y));
// 在健全性存在的情況下,不可取的選擇
listenEvent(EventType.Mouse, (e: Event) =>
console.log((e as MyMouseEvent).x + "," + (e as MyMouseEvent).y)
);
listenEvent(EventType.Mouse, ((e: MyMouseEvent) =>
console.log(e.x + "," + e.y)) as (e: Event) => void);
// 仍然不允許(明確的錯誤)。對於完全不相容的型別強制執行型別安全
listenEvent(EventType.Mouse, (e: number) => console.log(e));
當這種情況發生時,你可以讓TypeScript通過編譯器標誌 strictFunctionTypes
引發錯誤。
6.5 可選引數和其他引數
在比較函式的相容性時,可選引數和必需引數是可以互換的。源型別的額外可選引數不是錯誤,而目標型別的可選引數在源型別中沒有對應的引數也不是錯誤。
當一個函式有一個剩餘引數時,它被當作是一個無限的可選引數系列。
從型別系統的角度來看,這是不健全的,但從執行時的角度來看,可選引數的概念一般不會得到很好的加強,因為在這個位置傳遞 undefined
的引數對大多數函式來說是等價的。
激勵性的例子是一個函式的常見模式,它接受一個回撥,並用一些可預測的(對程式設計師)但未知的(對型別系統)引數數量來呼叫它。
function invokeLater(args: any[], callback: (...args: any[]) => void) {
/* ... 用'args'呼叫回撥 ... */
}
// 不健全 - invokeLater "可能 "提供任何數量的引數
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));
// 令人困惑的是(x和y實際上是需要的),而且是無法發現的
invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));
6.6 帶有過載的函式
當一個函式有過載時,源型別中的每個過載必須由目標型別上的相容簽名來匹配。這保證了目標函式可以在所有與源函式相同的情況下被呼叫。
6.7 列舉
列舉與數字相容,而數字與列舉相容。來自不同列舉型別的列舉值被認為是不相容的。比如說:
enum Status {
Ready,
Waiting,
}
enum Color {
Red,
Blue,
Green,
}
let status = Status.Ready;
status = Color.Green; // 錯誤
6.8 類
類的工作方式與物件字面型別和介面類似,但有一個例外:它們同時具有靜態和例項型別。當比較一個類型別的兩個物件時,只有例項的成員被比較。靜態成員和建構函式不影響相容性。
class Animal {
feet: number;
constructor(name: string, numFeet: number) {}
}
class Size {
feet: number;
constructor(numFeet: number) {}
}
let a: Animal;
let s: Size;
a = s; // 正確
s = a; // 正確
6.9 類中的私有和受保護成員
一個類中的私有成員和保護成員會影響其相容性。當一個類的例項被檢查相容性時,如果目標型別包含一個私有成員,那麼源型別也必須包含一個源自同一類的私有成員。同樣地,這也適用於有保護成員的例項。這允許一個類與它的超類進行賦值相容,但不允許與來自不同繼承層次的類進行賦值相容,否則就會有相同的形狀。
6.10 泛型
因為TypeScript是一個結構化的型別系統,型別引數只在作為成員型別的一部分被消耗時影響到結果型別。比如說:
interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;
x = y; // 正確,因為y符合x的結構
在上面, x
和 y
是相容的,因為它們的結構沒有以區分的方式使用型別引數。通過給 Empty
增加一個成員來改變這個例子,顯示了這是如何工作的。
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // 錯誤,因為x和y不相容
這樣一來,一個指定了型別引數的泛型型別就像一個非泛型型別一樣。
對於沒有指定型別引數的泛型,相容性的檢查是通過指定any來代替所有未指定的型別引數。然後產生的型別被檢查是否相容,就像在非泛型的情況下一樣。
比如說:
let identity = function <T>(x: T): T {
// ...
};
let reverse = function <U>(y: U): U {
// ...
};
identity = reverse; // 正確, 因為 (x: any) => any 匹配 (y: any) => any
6.11 子型別與賦值
到目前為止,我們已經使用了 "相容",這並不是語言規範中定義的一個術語。在TypeScript中,有兩種相容性:子型別和賦值。這些不同之處只在於,賦值擴充套件了子型別的相容性,允許賦值到 any
,以及賦值到具有相應數值的 enum
。
語言中不同的地方使用這兩種相容性機制中的一種,取決於情況。在實際應用中,型別相容性是由賦值相容性決定的,即使是在 implements
和 extends
子句中。
6.12 any
,unknown
,object
,void
,undefined
,null
, 和 never
可分配性
下表總結了一些抽象型別之間的可分配性。行表示每個型別可被分配到什麼,列表示什麼可被分配到它們。"✓"表示只有在關閉 strictNullChecks
時才是相容的組合
any | unknown | object | void | undefined | null | never | |
---|---|---|---|---|---|---|---|
any | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | |
unknown | ✓ | ✕ | ✕ | ✕ | ✕ | ✕ | |
object | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
void | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
undefined | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | |
null | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | |
never | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
- 所有的東西都是可以分配給自己的。
-
any
和unknown
在可分配的內容方面是相同的,不同的是unknown
不能分配給任何東西,除了any
。 -
unknown
和never
就像是彼此的反義詞。一切都可以分配給unknown
,never
就可以分配給一切。沒有任何東西可以分配給never
,unknown
不能分配給任何東西(除了any
)。 -
void
不能賦值給任何東西,以下是例外情況:any
、unknown
、never
、undefined
和null
(如果strictNullChecks
是關閉的,詳見表)。 - 當
strictNullChecks
關閉時,null
和undefined
與never
類似:可賦值給大多數型別,大多數型別不可賦值給它們。它們可以互相賦值。 - 當
strictNullChecks
開啟時,null
和undefined
的行為更像void
:除了any
、unknown
、never
和void
之外,不能賦值給任何東西(undefined
總是可以賦值給void
)。
TypeScript學習高階篇第七章:迭代器和生成
7.1 遍歷
如果一個物件有 Symbol.iterator
屬性的實現,它就被認為是可迭代的。一些內建型別,如 Array
、 Map
、 Set
、 String
、 Int32Array
、 Uint32Array
等,已經實現了它們的 Symbol.iterator
屬性。物件上的 Symbol.iterato
r 函式負責返回要迭代的值的列表。
7.1.1 Iterable
介面
Iterable
是一個我們可以使用的型別,如果我們想接收上面列出的可迭代的型別。下面是一個例子:
// 傳入的引數必須是可迭代的型別
function toArray<X>(xs: Iterable<X>): X[] {
return [...xs]
}
7.1.2 for ... of
宣告
for... of
在一個可迭代物件上迴圈,呼叫物件上的 Symbol.iterator
屬性。下面是一個關於陣列的簡單 for... of
迴圈。
let someArray = [1, "string", false];
for (let entry of someArray) {
console.log(entry); // 1, "string", false
}
7.1.3 for ... of
與for ... in
宣告
for...of
和 for...in
語句都是在列表上進行迭代;但迭代的值是不同的, for...in
返回被迭代物件的鍵值列表,而 for...of
返回被迭代物件的數字屬性值列表。 這裡有一個例子可以證明這種區別:
let list = [4, 5, 6];
for (let i in list) {
console.log(i); // "0", "1", "2",
}
for (let i of list) {
console.log(i); // 4, 5, 6
}
另一個區別是 for...in
對任何物件進行操作;它作為一種檢查該物件上的屬性的方法。另一方面,for...of
主要對可迭代物件的值感興趣。像 Map
和 Set
這樣的內建物件實現了 Symbol.iterator
屬性,允許訪問儲存的值。
// Set中的Iterable
let pets = new Set(["Cat", "Dog", "Hamster"]);
for (let pet in pets) {
console.log(pet); // 什麼也不輸出
}
for (let pet of pets) {
console.log(pet); // "Cat", "Dog", "Hamster"
}
// Map中的Iterable
let nums = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
])
for (let num in nums) {
console.log(num) // 什麼也不輸出
}
for (let num of nums) {
console.log(num) //[ 1, 'one' ] [ 2, 'two' ] [ 3, 'three' ]
}
7.2 程式碼生成
7.2.1 生成目標 ES5 和 ES3
當針對ES5或ES3相容的引擎時,迭代器只允許在 Array
型別的值上使用。
在非陣列值上使用 for...of
迴圈是一個錯誤,即使這些非陣列值實現了 Symbol.iterator
屬性。
例如,編譯器將為 for...
的迴圈生成一個簡單的 for
迴圈。
let numbers = [1, 2, 3];
for (let num of numbers) {
console.log(num);
}
將被生成為:
var numbers = [1, 2, 3];
for (var _i = 0; _i < numbers.length; _i++) {
var num = numbers[_i];
console.log(num);
}
7.2.2 ECMAScript 2015(ES6) 和 更高版本
當針對ECMAScipt 2015相容的引擎時,編譯器將生成 for...of
迴圈,以針對引擎中的內建迭代器實 現。
TypeScript學習高階篇第八章:裝飾器(Decorators)
8.1 簡介
隨著TypeScript和ES6中類的引入,現在存在某些場景需要額外的功能,來支援註釋或修改類和類成員。 裝飾器提供了一種為類宣告和成員添加註釋和超程式設計語法的方法。裝飾器是JavaScript的第二階段建議,並作為TypeScript的一個實驗性功能提供。
注意:裝飾器是一個實驗性的功能,在未來的版本中可能會改變。
要啟用對裝飾器的實驗性支援,你必須在命令列或在 tsconfig.json
中啟用experimentalDecorators
編譯器選項。
- 命令列開啟
tsc --target ES5 --experimentalDecorators
- tsconfig.json
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
8.2 裝飾器
裝飾器是一種特殊的宣告,可以附加到類宣告、方法、訪問器、屬性或引數上。裝飾器使用 @expression
的形式,其中 expression
必須評估為一個函式,該函式將在執行時被呼叫,並帶有關於被裝飾的宣告的資訊。
例如,對於裝飾器 @sealed
,我們可以將 sealed
的函式寫成如下:
function sealed(target) {
// 對 "target"做一些事情 ...
}
8.3 裝飾器工廠
如果我們想自定義裝飾器如何應用於宣告,我們可以寫一個裝飾器工廠。裝飾器工廠是一個簡單的函式,它返回將在執行時被裝飾器呼叫的表示式。
我們可以用以下方式寫一個裝飾器工廠:
function color(value: string) {
// 這是裝飾器工廠,它設定了
// 返回的裝飾器函式
return function (target) {
// 這就是裝飾器
// 用 "target" 和 "value"做一些事情...
};
}
8.4 裝飾器構成
多個裝飾器可以應用於一個宣告,例如在一行中:
@f @g x
多行的語法:
@f
@g
x
當多個裝飾器適用於一個宣告時,它們的評估類似於數學中的函式組合。在這種模式下,當組合函式f和g時,所產生的組合 (f(g))(x)
等同於 f(g(x))
。
因此,在TypeScript中對一個宣告的多個裝飾器進行評估時,會執行以下步驟:
- 每個裝飾器的表示式都是自上而下地進行評估的。
- 然後將結果作為函式從下往上呼叫。
如果我們使用裝飾器工廠,可以通過下面的例子觀察這個評估順序:
function first() {
console.log("first(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): called");
};
}
class ExampleClass {
@first()
@second()
method() {}
}
這將把這個輸出列印到控制檯:
first(): factory evaluated
second(): factory evaluated
second(): called
first(): called
8.5 裝飾器評估
對於應用於類內各種宣告的裝飾器,有一個明確的順序:
- 對於每個例項成員,首先是引數裝飾器,然後是方法、訪問器或屬性裝飾器。
- 對於每個靜態成員,先是引數裝飾器,然後是方法、存取器或屬性裝飾器。
- 引數裝飾器被應用於建構函式。
- 類裝飾器適用於類。
8.6 類裝飾器
類裝飾器就在類宣告之前被宣告。類裝飾器被應用於類的建構函式,可以用來觀察、修改或替換類定義。類裝飾器不能在宣告檔案中使用,也不能在任何其他環境下使用(比如在 declare
類上)。
類裝飾器的表示式在執行時將作為一個函式被呼叫,被裝飾的類的構造器是它唯一的引數。
如果類裝飾器返回一個值,它將用提供的建構函式替換類宣告。
注意:如果你選擇返回一個新的建構函式,必須注意維護原始原型。在執行時應用裝飾器的邏輯不 會為你這樣做。
下面是一個應用於 BugReport
類的類裝飾器( @sealed
)的例子。
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
我們可以用下面的函式宣告來定義@sealed
裝飾器
// Object.seal()方法封閉一個物件,阻止新增新屬性並將所有現有屬性標記為不可配置。當前屬性的值只要原來是可寫的就可以改變。
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
當 @sealed
被執行時,它將同時封閉建構函式和它的原型,因此將阻止在執行時通過訪問 BugReport.prototype
或通過定義 BugReport
本身的屬性來向該類新增或刪除任何進一步的功能(注意ES2015類實際上只是基於原型的建構函式的語法糖)。這個裝飾器並不能阻止類對 BugReport
進行子類化。
接下來我們有一個如何覆蓋建構函式以設定新的預設值的例子:
function reportableClassDecorator<T extends { new (...args: any[]): {} }>(constructor: T){
return class extends constructor {
reportingURL = "http://www...";
};
}
@reportableClassDecorator
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
const bug = new BugReport("Needs dark mode");
console.log(bug.title); // 列印 "Needs dark mode"
console.log(bug.type); // 列印 "report"
// 注意,裝飾器不會改變TypeScript的型別
// 因此,型別系統對新的屬性`reportingURL`是不可知的。
bug.reportingURL;