TypeScript學習文件-基礎篇(完結)
- TypeScript學習第一章:TypeScript初識
- TypeScript學習第二章:為什麼使用TypeScript?
- TypeScript學習第三章: 常用型別
- TypeScript學習第四章: 型別縮小
-
TypeScript學習第五章: 函式
- TypeScript學習第六章: 物件型別
- TypeScript學習第七章: 型別操縱
- TypeScript學習第八章: 類
- TypeScript學習第九章:模組
TypeScript學習第一章:TypeScript初識
1.1 TypeScript學習初見
TypeScript(TS)是由微軟Microsoft由2012年推出的自由和開源的程式語言, 目前主流的三大框架React 、Vue 和 Angular這三大主流框架再加上最新的鴻蒙3.0都可以用TS進行開發.
可以說 TS 是 JS 的超集, 是建立在JavaScript上的語言. TypeScript把其他語言的一些精妙的語法, 帶入到JavaScript中, 讓JS達到了一個新的高度。
可以在TS中使用JS以外的擴充套件語法, 同時可以結局TS對面向物件和靜態型別的良好支援, 可以讓我們編寫更健壯、更可維護的大型專案。
1.2 TypeScript介紹
因為TypeScript是JavaScript的超集, 所以要介紹TS, 不得不提一下JS, JS從在引入程式設計社群20多年以來, 已經成了有史以來應用最廣泛的跨平臺語言之一了, 從一開始為網頁中新增一些微不足道的、互動性的小型的指令碼語言發展到現在各種規模的前端和後端應用程式的首選語言了.
雖然我們用JS語言編寫程式的大小、範圍和複雜性呈指數級的增長, 但是JS語言表達不同程式碼單元之間的關係和能力卻很弱, 使得JS成了一項難以大規模管理的任務, 而且也很難解決程式設計師經常出現的錯誤: 型別錯誤.
而TS語言可以很好的解決這個錯誤, 他的目標是成為JS程式的靜態型別檢查器, 可以在程式碼執行之前進行檢查, 也就是靜態編譯, 並且呢, 可以確保我們程式的型別正確(即進行型別檢查).
TS添加了可選的靜態型別和基於類的面向物件程式設計等等, 是JS的語言擴充套件, 不是JS的替代品, 會讓JS前進的步伐更堅實、更遙遠.
1.3 JS 、TS 和 ES之間的關係
ES6又稱為ECMAScript 2015, TypeScript 是 JS 的超集, 他包含Javascript的所有元素, 能執行Javascript程式碼, 並擴充套件了JS語法, 並添加了靜態型別 類 模組 介面 型別註解等等方面的功能, 更加易於大專案的開發.
這張圖表示TS不僅包含了JS和ES的最新內容, 還擴充套件了新的功能.
總的來說, ECMAScript是JS的標準, TS是JS的超集.
1.4 TS的競爭者有哪些?
1. ESLint
2. TSlint
1 和 2 都是和TypeScript一樣來突出程式碼中可能出現的錯誤, 至少i沒有為檢查過程新增新的語法, 但是這兩者都不打算最為IDE整合的工具來執行, 這兩個的存在可以是TS做更少的檢查, 但是這些檢查並不適合於所有的程式碼庫。
3. CoffeeScript
CoffeeScript是想改進JS語言, 但是現在用的人少了, 因為他又成為了JS的標準, 屬於是打不過JS了。
4.Flow
Vue2的原始碼的型別檢查工具就是flow, 不過Vue3已經開始使用TS做型別檢查了.
Flow更悲觀的判斷型別, 而TS更加樂觀.
Flow是為了維護Facebook的程式碼庫而建立的, 而TS是作為一種獨立的語言而建立的, 其內部有獨立的環境, 可以自由專注於工具的開發和整個生態系統的維護
TypeScript學習第二章:為什麼使用TypeScript?
2.1 發現問題
JS中每個值都有一組行為, 我們可以通過執行不同的操作來觀察:
// 在 'message' 上訪問屬性方法 'toLowerCase', 並呼叫它
message.toLowerCase();
// 呼叫 'message'
message();
我們嘗試直接呼叫message, 但是假設我們不知道message, 我們就無法可靠的說出嘗試執行任何的這些程式碼會得到什麼結果, 每個操作的結果完全取決於我們最初給message的賦值. 我們編譯程式碼的時候真的可以呼叫message()麼, 也不一定有toLowerCase()這個方法, 而且也不知道他們的返回值是什麼.
通常我們在編寫js的時候需要對上面所述的細節牢記在心, 才能編寫正確的程式碼。
假設我們知道了message 是什麼,如下所示,但是第三行就會報錯。
const message = 'Hello World'
message.toLowerCase(); // 輸出hello world
message(); // TypeError: message is not a function
如果我們能避免這樣的錯誤, 就完美了, 當我們執行我們的程式碼的時候, 選擇做什麼的方式, 是通過確定值的型別, 來確定他具有什麼樣的行為和功能的, TypeError
就暗指字串是不能作為函式來呼叫的. 對於某些值, 比如string
和number
, 我們可以使用typeof來識別他們的型別.
但是對於像函式之類的其他的東西, 沒有相應的執行時機制, 比如下面的程式碼, 執行是有條件的, 也就是說這個x是必須具有flip這個方法的, js只能在執行一下程式碼時才能知道這個x是提供了什麼的, 我們如果能夠使用靜態型別系統, 在執行程式碼之前預測預期的程式碼,問題就解決了.
function fn(x) {
return x.flip()
}
2.2 靜態型別檢查
const message = 'hello'
message() // TypeError
上述這段程式碼會引起TypeError, 理想的情況下, 我們希望有一個工具可以在我們程式碼執行之前發現這些錯誤, TS就可以實現這些功能. 靜態型別系統就描述了當前我們執行程式的時候, 值得形狀和行為, 像TS這樣的型別檢查器, 會告訴我們什麼時候程式碼會出現問題.
2.3 非異常故障
JS 在執行的時候會告訴我們他認為某些東西是沒有意義的情況, 因為ECMA規範明確說明了JS在遇到某些意外情況下應該是如何表現得, 比如如下程式碼:
const user = {
name: "小千",
age:26,
};
user.location; // 返回undefined, 理應報錯, 因為根本沒有location這個屬性
但是靜態型別系統要求必須對呼叫哪些程式碼做系統的標記, 如果是在TS執行這段程式碼, 就會出現location未定義的錯誤, 如下圖所示:
TS可以在開發過程中捕獲很多類似於合法的錯誤, 比如說錯別字, 未呼叫函式, 基本的邏輯錯誤等等:
拼寫錯誤: 屬性toLocaeleLowerCase在String型別中不存在, 你找的是否是toLocaleLowerCase屬性?
未呼叫的函式檢查: 運算子號 < 不能用在一個 '() => number' 和 number數字之間.
邏輯問題: value !== 'a' 和 value === 'b'邏輯重疊.
2.4 使用工具
- 安裝VSCode
- 安裝Node.js:使用命令
node -v
來檢查nodejs版本 - 安裝TypeScript編譯器:
npm i typescript -g
然後我們要編譯我們的TS, 因為TS是不能直接執行的, 我們必須把他編譯成JS.
在終端中使用cls 或者 clear命令可以清屏
可以使用tsc命令來轉換TS 成 JS: 例如 tsc hello.ts
, 就會生成對應的JS檔案.
hello.ts:
// 你好, 世界
// console.log('Hello World')
// 會出現函式實現重複的錯誤
function greet(person, date) {
console.log(`Helo ${person}, today is ${date}`)
}
greet('xiaoqian','2021/12/04')
會出現函式實現重複的錯誤是因為hello.js也有這個greet的函式, 這是跟我們編譯環境是矛盾的, 而且還需要我們重新編譯ts, 所以我們需要進行優化編譯過程.
2.5 優化編譯
- 解決TS和JS衝突問題
tsc --init # 生成配置檔案
- 自動編譯
tsc --watch
- 發出錯誤
tsc --noEmitOnError hello.ts
TS檔案編譯成JS檔案以後, 當出現函式名或者是變數名相同的時候, 會給我們提示重複定義的問題,可以通過 tsc --init
來生成一個配置檔案來解決衝突問題. 先把嚴格模式strict關閉, 可解決未指定變數型別的問題.
當我們修改TS檔案的時候, 我們需要重新的執行編譯, 才能拿到最新的結果我們需要自動編譯, 可以通過tsc --watch
來解決自動編譯的問題.
當我們編譯完之後, JS還是能正常執行的, 我們可以加一個noEmitOnError的引數來解決, 這樣的話如果我們在TS中出現錯誤就可以讓TS不編譯成JS檔案了.
最終的命令列指令是這樣的:
tsc --watch --noEmitOnError
2.6 顯式型別
剛才我們在tsconfig.json裡把strict模式關閉了, 如果我們開啟, 就會出現未指定變數型別的錯誤, 如果要解決這個問題, 我們就需要指定顯式型別:
什麼叫顯式型別呢, 就是手工的給變數定義型別, 語法如下:
function greet(person: string, date: Date) {
console.log(`Helo ${person}, today is ${date.toDateString()}.`)
}
在TS中, 也不是必須指定變數的資料型別, TS會根據你的變數自動推斷資料型別, 如果推斷不出來就會報錯.
2.7 降級編譯
我們可以在tsconfig.json 就修改target來更改TS編譯目標的程式碼版本.
{
"compilerOptions": {
......
"target": 'es5',
......
}
}
預設為es2016, 即es7, 建議以預設值就可以, 目前的瀏覽器都能相容
2.8 嚴格模式
不同的使用者使用TS在型別檢查中希望檢查的嚴格程度是不同的, 有的人喜歡更寬鬆的驗證體驗, 從而僅僅驗證程式的某些部分, 並且仍然擁有不錯的工具.
預設情況下:
{
"compilerOptions": {
......,
"strict": true, /* 嚴格模式: 啟用所有嚴格的型別檢查選項。*/
"noImplicitAny": true, /* 為隱含的'any'型別的表示式和宣告啟用錯誤報告。*/
"strictNullChecks": true, /* 當型別檢查時,要考慮'null'和'undefined' */
......
}
}
一般來說使用TS就是追求的強立即驗證, 這些靜態檢查設定的越嚴格, 越可能需要更多額外的程式設計工作, 但是從長遠來說是值得的, 它會使程式碼更加容易維護. 如果可以我們應該始終開啟這些型別檢查.
啟用strictNullChecks可以攔截null 和undefined 的錯誤, 啟用noImplicitAny可以攔截any的錯誤, 啟用strict可以攔截所有的嚴格型別檢查選項, 包括前面兩個的.
所以結論就是隻需要開啟"strict"為true即可, 當我們遇到
TypeScript學習第三章: 常用型別
3.1 基元型別string number 和 boolean
- string: 字串, 例子: 'Hello', 'World'.
- number: 數字, 例子: 42, -100.
- boolean: 布林, 例子: true, false.
String Number Boolean 也是合法的, 在TS裡專門指一些很少的, 出現在程式碼裡的一些特殊的內建型別, 對於型別我們始終使用小寫的string, number 和 boolean.
為了輸出方便我們可以在tsconfig.json的rootDir裡設定一個目錄"./src"
, 設定outDir為"./dist"
.
let str: string = 'hello typescript'
let num: number = 100
let bool: boolean = true
3.2 陣列
陣列的定義方法有兩種:
- type[]
- Array
Array
let arr: number[] = [1, 4, 6 ,8]
// arr = ['a']
let arr2: Array<number> = [1, 2, 3]
arr2 = []
值得注意的是, 陣列可以被賦值為空陣列[], 但是不能被賦值為規定型別以外的陣列值.
3.3 any
如果不希望某個特定值導致型別檢查錯誤, 就可以使用any.
當一個值是any的時候, 可以訪問它的任何屬性, 將它分配給任何型別的值, 或者幾乎任何其它語法上的東西都是合法的. 但是執行的時候該報錯還是報錯, 所以我們不應該經常使用他.
let obj: any = {
x: 0
}
obj.foo() // js呼叫時就會報錯
obj()
obj.bar = 100
obj = 'hello'
const n: number = obj
3.4 變數上的型別解釋
let myName: string = "Felixlu"
採用(冒號:) + (型別string)的方式.
let my: string = "Hello World"
// 如果不宣告, 會自動推斷
let myName = "Bleak" // 將myName推斷成string
myName = 100 // 報錯, 不能將number分配給string.
3.5 函式
function greet (name: string): void {
console.log("Hello," + name.toUpperCase() + "!!!")
}
const greet2 = (name: string): string =>{
return "你好," + name
}
greet("Bleak")
console.log(greet2("黯淡"))
第一個name: string是引數型別註釋, 第二個: void是返回值型別註釋.
一般來說不用定義返回值型別, 因為會自動推斷.
const names = ["xiaoqian", 'xiaoha', 'xiaoxi']
names.forEach(function(s) {
console.log(s.toUpperCase());
})
names.forEach(s => {
console.log(s.toLowerCase());
})
匿名函式與函式宣告有點不同, 當一個函數出現在出現在TS可以確定它如何被呼叫的地方的時候, 這個函式的引數會自動的指定型別.
3.6 物件型別
function printCoord(pt: {x: number; y: number}) {
console.log("座標的x值是: " + pt.x)
console.log("座標的y值是: " + pt.y)
}
printCoord({x: 3, y: 7})
對於引數型別註釋是物件型別的, 物件中屬性的分割可以用 分號; 或者 逗號,
function printName(obj: {first: string, last?: string}) {
if(obj.last === undefined) {
console.log("名字是:" + obj.first)
} else {
console.log("名字是:" + obj.first + obj.last)
}
}
printName({
first: "Mr.",
last: "Bleak"
})
使用?可以指定物件中某個引數可以選擇傳入或者不傳入, 不傳入其值就是undefined.
如何在函式體內確定某個帶?的引數是否傳參了呢?可以使用兩種方法
-
if(obj.last === undefined) {// 未傳入時的方法體 } else {// 傳入時的方法體 }
-
console.log(obj.last?.toUpperCase())
第二種方式更加優雅, 更推薦使用
3.7 聯合型別
let id: number | string
TS的型別系統允許我們使用多種運算子, 從現有型別中構建新型別union.
聯合型別是由兩個或多個其他型別組成的型別. 表示可能是這些型別中的任何一種的值, 這些型別中的每一種被稱為聯合型別的成員.
function printId(id: number | string) {
console.log("當前Id為:" + id)
// console.log(id.toUpperCase())
if (typeof id === 'string') {
console.log(id.toUpperCase())
} else {
console.log(id)
}
}
printId(101)
printId('202')
如果需要呼叫一些引數的屬性或者方法, 可以使用JS攜帶的typeof函式來進行判斷並分情況執行程式碼.
function welcomePeople(x: string[] | string) {
if(Array.isArray(x)) { // Array.isArray(x)可以測試x是否是一個數組
console.log("Hello, " + x.join(' and '))
} else {
console.log("Welcome lone traveler " + x)
}
}
welcomePeople(["A", "B"])
welcomePeople('A')
根據分支來進行操作的函式.
// 共享的方法
function getFirstThree(x: number[] | string) {
return x.slice(0, 3)
}
都有的屬性和方法, 可以直接使用.
3.8 類型別名
type Point = {
x: number
y: number
} // 物件型別
function printCoord(pt: Point) {
}
printCoord({x: 100, y: 200})
type ID = number | string // 聯合型別
function printId(id: ID) {
}
printId(100)
printId('2333')
type UserInputSanitizedString = string // 基元型別
function sanitizedString(str: string): UserInputSanitizedString {
return str.slice(0, 2)
}
let userInput = sanitizedString('hello')
console.log(userInput)
type可以用來定義變數的型別, 如果是物件, 裡面的屬性和方法可以用逗號, 分號; 或直接不寫來做間隔, 可以用來做一些平時經常會用到的型別來做複用, 其可以用於變數的型別指定上.
3.9 介面
interface Point {
x: number;
y: number;
}
function printCoord(pt: Point) {
console.log("座標x的值是: " + pt.x)
console.log("座標y的值是: " + pt.y);
}
printCoord({ x: 100, y: 100 })
可以用介面來定義物件的型別, 幾乎所有可以通過interface來定義的型別都可以用type來定義
類型別名type 和介面interface之間的區別:
- 擴充套件介面: 通過extends
// 擴充套件介面
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
const bear: Bear = {
name: 'winie',
honey: true
}
console.log(bear.name, bear.honey)
擴充套件類型別名: 通過 &
type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}
const bear: Bear = {
name: "winie",
honey: true
}
-
向現有的型別新增新欄位
介面: 定義相同的介面, 其欄位會合並.
interface MyWindow {
count: number
}
interface MyWindow {
title: string
}
const w: MyWindow = {
title: 'hello ts',
count: 10
}
類型別名: 類型別名建立的型別建立後是不能新增新欄位的
3.10 型別斷言 as
const myCanvas = document.getElementById("main_canvas") // 返回某種型別的HTMLElement
// 可以使用型別斷言來指定
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement
const myCanvas = <HTMLCanvasElement>document.getElementById()
型別註釋與型別斷言一樣, 型別斷言由編譯器來刪除, 不會影響程式碼的執行時行為, 也就是因為型別斷言在編譯時被刪除, 所以沒有與型別斷言相關聯的執行時檢查.
const x = ('hello' as unknown) as number
如上程式碼可以在我們不知道某些程式碼是什麼型別的時候斷言為一個差不多的型別.
3.11 文字型別
除了一般型別string
和number
, 還可以在型別位置引用特定的字串和數字.
一種方法是考慮js如何以不同的方式宣告變數. var
和let
兩者都允許更改變數中儲存的內容, const
不允許, 這反映在TS如何為文字建立型別上
let testString = "Hello World";
testString = "Olá Mundo";
// 'testString'可以表示任何可能的字串,那TypeScript是如何在型別系統中描述它的
testString;
const constantString = "Hello World";
// 因為'constantString'只能表示1個可能的字串,所以具有文字型別表示
constantString;
就其本身而言, 文字型別不是很有價值
let x: "hello" = "hello";
// 正確
x = "hello"
// 錯誤
x = "howdy"
擁有一個只能由一個值的變數並沒有多大用處!
但是通過將文字組合成聯合,你可以表達一個更有用的概念——例如,只接受一組特定已知值的函式:
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
數字文字型別的工作方式相同:
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
也可以將這些與非文字型別結合使用:
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
還有一種文字型別:布林文字。只有兩種布林文字型別,它們是型別 true
和 false
。型別 boolean 本身實際上只是聯合型別 union
的別名 true | false
。
文字推理
當你使用物件初始化變數時,TypeScript 假定該物件的屬性稍後可能會更改值。例如,如果你寫了這樣的程式碼:
const obj = { counter: 0};
if(someCondtion) {
obj.counter = 1
}
TypeScript 不假定先前具有的欄位值 0
,後又分配 1
是錯誤的。另一種說法是 obj.counter
必須有 number
屬性, 而非是 0
,因為型別用於確定讀取和寫入行為。
這同樣適合用於字串:
function handleRequest(url: string, method: 'GET' | 'POST' | 'GUESS') {
// ...
}
const req = { url: 'https://example.com', method: 'GET' };
handleRequest(req.url, req.method);
在上面的例子 req.method
中推斷是 string
,不是 "GET"
。因為程式碼可以在建立 req
和呼叫之間進行評估,TypeScript 認為這段程式碼有錯誤。
有兩種方法可以解決這個問題:
- 可以通過在任一位置新增型別斷言來更改推理:
// 方案 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// 方案 2
handleRequest(req.url, req.method as "GET");
方案1表示“我打算 req.method 始終擁有文字型別"GET"
”,從而防止之後可能分配"GUESS"
給該欄位。
方案 2 的意思是“我知道其他原因req.method
具有"GET"
值”。
- 可以使用
as const
將整個物件轉換為型別文字
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
該as const
字尾就像const
定義,確保所有屬性分配的文字型別,而不是一個更一般的string
或 number
。
3.12 null
和undefined
JavaScript 有兩個原始值用於表示不存在或未初始化的值: null
和 undefined
.
TypeScript有兩個對應的同名型別。這些型別的行為取決於您是否在tsconfig.json
設定strictNullChecks
選擇。
-
strictNullChecks
關閉使用false,仍然可以正常訪問的值,並且可以將值分配給任何型別的屬性。這類似於沒有空檢查的語言 (例如 C#、Java)的行為方式。缺乏對這些值的檢查往往是錯誤的主要來源;如果在他們的程式碼庫中這樣做可行,我們總是建議大家開啟。
-
strictNullChecks
開啟使用true,你需要在對該值使用方法或屬性之前測試這些值。就像在使用可選屬性之前檢查一樣,我們可以使用縮小來檢查可能的值:
function doSomething(x: string | null) {
if (x === null) {
// 做一些事
} else {
console.log("Hello, " + x.toUpperCase());
}
}
-
非空斷言運算子(
!
字尾)
TypeScript 也有一種特殊的語法 null
, undefined
, 可以在不進行任何顯式檢查的情況下,從型別中移除和移除型別。 !
在任何表示式之後寫入實際上是一種型別斷言,即該值不是 null
or undefined
:
使用?
可以指定物件中某個引數可以選擇傳入或者不傳入, 不傳入其值就是undefined.
function liveDangerously(x?: number | null) {
// 正確
console.log(x!.toFixed());
}
就像其他型別斷言一樣,這不會更改程式碼的執行時行為,因此僅 !
當你知道該值不能是 null
或 undefined
時使用才是重要的。
3.13 列舉
列舉是 TypeScript 新增到 JavaScript 的一項功能,它允許描述一個值,該值可能是一組可能的命名常量之一。與大多數 TypeScript 功能不同,這不是JavaScript 的型別級別的新增,而是新增到語言和執行時的內容。因此,你確定你確實需要列舉在做些事情,否則請不要使用。可以在Enum參考頁中閱讀有關列舉的更多資訊。
// ts原始碼
enum Direction {
Up = 1,
Down,
Left,
Right,
}
console.log(Direction.Up) // 1
// 編譯後的js程式碼
"use strict";
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 1] = "Up";
Direction[Direction["Down"] = 2] = "Down";
Direction[Direction["Left"] = 3] = "Left";
Direction[Direction["Right"] = 4] = "Right";
})(Direction || (Direction = {}));
console.log(Direction.Up);
3.14 不太常見的原語
值得一提的是JavaScript中一些較新的原語, 它們在 TypeScript 型別系統中也實現了。我們先簡單的看兩個例子:
bigint
從 ES2020(ES11) 開始,JavaScript 中有一個用於非常大的整數的原語BigInt
:
// 通過bigint函式建立bigint
const oneHundred: bigint = BigInt(100);
// 通過文字語法建立BigInt
const anotherHundred: bigint = 100n;
你可以在TypeScript 3.2發行說明中瞭解有關 BigInt 的更多資訊。
symbol
JavaScript 中有一個原語 Symbol()
,用於通過函式建立全域性唯一引用:
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
// 這裡的程式碼不可能執行
}
此條件將始終返回 false
,因為型別typeof firstName
和typeof secondName
沒有重疊。
TypeScript學習第四章: 型別縮小
假設我們有一個名為padLeft的函式:
function padLeft(padding: number | string, input: string): string {
throw new Error("尚未實現!");
}
我們來擴充一下功能: 如果padding
是number
, 它會將其視為我們將要新增到input
的空格數; 如果padding
是string
, 它只在input上做padding
. 讓我們嘗試實現:
function padLeft(padding: number | string, input: string): string {
return new Array(padding + 1).join(" ") + input;
}
這樣的話, 我們在padding + 1處會遇到錯誤. TS警告我們, 運算子+不能應用於型別number | string
和 string
, 這個邏輯是對的, 因為我們沒有明確檢查padding是否為number
, 也沒有處理它是string
的情況, 所以我們我們這樣做:
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
}
return padding + input;
}
如果這大部分看起來像無趣的JavaScript程式碼,這也算是重點吧。除了我們設定的註解之外,這段 TypeScript程式碼看起來就像JavaScript。
我們的想法是,TypeScript的型別系統旨在使編寫典型的 JavaScript程式碼變得儘可能容易,而不需要彎腰去獲得型別安全。
雖然看起來不多,但實際上有很多價值在這裡。就像TypeScript使用靜態型別分析執行時的值一樣,它在JavaScript的執行時控制流構造上疊加了型別分析,如if/else、條件三元組、迴圈、真實性檢查等,這些都會影響到這些型別。
在我們的if檢查中,TypeScript看到typeof padding ==="number"
,並將其理解為一種特殊形式的程式碼,稱為型別保護。TypeScript遵循我們的程式可能採取的執行路徑,以分析一個值在特定位置的最具體的可能型別。它檢視這些特殊的檢查(稱為型別防護)和賦值,將型別細化為比宣告的更具體的型別的過程被稱為型別縮小。在許多編輯器中,我們可以觀察這些型別的變化,我們甚至會在我們的例子中這樣做。
TypeScript 可以理解幾種不同的縮小結構.
4.1 typeof
型別守衛
正如我們所見, Js支援typeof
運算子, 它可以提供有關我們在執行時擁有的值型別的非常基本的資訊.
TS期望它返回一組特定的字串:
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"
就像我們剛才在padLeft
中看到的那樣, 這個運算子經常出現在許多JavaScript庫中, TS可以理解為, 它縮小在不同分支中的型別.
在TS中, 檢查typeof
的返回值是一種型別保護. 因為TS對typeof
操作進行編碼, 從而返回不同的值, 所以它知道對JS做了什麼. 例如, 請注意上面的列表中, typeof
不返回null
.
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// 做點事
}
}
在 printAll
函式中,我們嘗試檢查 strs
是否為物件,來代替檢查它是否為陣列型別(現在可能是強調陣列是 JavaScript 中的物件型別的好時機)。但事實證明,在 JavaScript 中, typeof null
實際上也是 "object"
! 這是歷史上的不幸事故之一。
有足夠經驗的使用者可能不會感到驚訝,但並不是每個人都在 JavaScript 中遇到過這種情況;幸運的是, ts 讓我們知道, strs
只縮小到 string[] | null
,而不僅僅是 string[].
這可能是我們所謂的“真實性”檢查的一個很好的過渡。
4.2 真值縮小
真值檢查是我們在JS中經常做的一件事. 在JS中, 我們可以在條件 &&
||
if
語句布林否定(!)等中使用任何表示式.
例如, if
語句不希望它們的條件總是具有型別boolean
function getUserOnlineMessage(numUserOnline: number) {
if(numUserOnline) {
return `現在共有 ${numUserOnline} 人線上!`
}
return "現在沒有人線上:("
}
在JS總, if條件語句, 首先把他們的條件強制轉化為boolean以使其有意義, 然後根據結果是true還是false來選擇他們的分支. 像下面這些值都強制轉換為false:
- 0
- NaN
- "" (空字串)
- On (bigint 0的版本)
- null
- undefined
其他值被強制轉化為true
. 你始終可以在Boolean
函式中執行值獲得boolean
, 或使用較短的雙布林否定將值強制轉換為boolean
.(後者的優點是ts推斷出一個狹窄的文字布林型別true, 而將第一個推斷為boolean
型別)
// 這兩個結果都返回 true
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true, value: true
利用這個特性, 我們可以防範諸如null
或undefined
之類的值時. 例如, 讓我們嘗試將它用於我們的printAll
函式.
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
我們通過檢查strs
是否為真, 消除了上述錯誤. 這可以防止我們在執行程式碼的時候出現一些錯誤, 例如:
TypeError: null is not iterable
但請記住, 對原語的真值檢查通常容易出錯. 例如, 考慮改寫printAll
:
function printAll(strs: string | string[] | null) {
// !!!!!!!!!!!!!!!!
// 別這樣!
// 原因在下邊
// !!!!!!!!!!!!!!!!
if (strs) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}
我們將整個函式體包裹在一個真值檢查中, 但是這有一個小小的缺點: 我們可能不再正確處理空字串
的情況.
TS在這裡根本不會報錯, 如果你不熟悉JS, 這是值得注意的. TS通常可以幫你及早發現錯誤, 但是如果你選擇對某個值不做任何處理, 那麼它可以做的就只有這麼多, 而不會考慮過多邏輯方面的問題, 如果需要, 你可以確保linter(程式規範性)處理此類情況.
關於通過真實性縮小範圍的最後一點,是通過布林否定 !
把邏輯從否定分支中過濾掉。
function multiplyAll(
values: number[] | undefined,
factor: number
): number[] | undefined {
if (!values) {
return values;
} else {
return values.map((x) => x * factor);
}
}
4.3 等值縮小
ts也使用分支語句做===
!==
==
和 !=
等值檢查, 來實現型別縮小. 例如:
function example(x: string | number, y: string | boolean) {
if (x === y) {
// 現在可以在x,y上呼叫字串型別的方法了
x.toUpperCase();
y.toLowerCase();
} else {
console.log(x);
console.log(y);
}
}
當我們在上面的示例中檢查 x 和 y 是否相等時,TypeScript知道它們的型別也必須相等。由於 string 是 x 和 y 都可以採用的唯一常見型別,因此TypeScript 知道 x 、 y 如果都是 string
,則程式走第一個分支中 。
檢查特定的字面量值(而不是變數)也有效。在我們關於真值縮小的部分中,我們編寫了一個 printAll 容易出錯的函式,因為它沒有正確處理空字串。相反,我們可以做一個特定的檢查來阻止 null ,並且 TypeScript 仍然正確地從 strs 裡移除 null 。
function printAll(strs: string | string[] | null) {
if (strs !== null) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}
JavaScript 更寬鬆的相等性檢查 ==
和 !=
,也能被正確縮小。如果你不熟悉,如何檢查某個變數是否 == null
,因為有時不僅要檢查它是否是特定的值 null
,還要檢查它是否可能是 undefined
。這同樣適用 於 == undefined
:它檢查一個值是否為 null 或 undefined 。現在你只需要這個 ==
和 !=
就可以搞定了。
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
// 從型別中排除了undefined 和 null
if (container.value != null) {
console.log(container.value);
// 現在我們可以安全地乘以“container.value”了
container.value *= factor;
}
}
console.log(multiplyValue({value: 5}, 5))
console.log(multiplyValue({value: null}, 5))
console.log(multiplyValue({value: undefined}, 5))
console.log(multiplyValue({value: '5'}, 5))
4.4 in
操作符縮小
JavaScript 有一個運算子,用於確定物件是否具有某個名稱的屬性: in
運算子。TypeScript 考慮到了這 一點,以此來縮小潛在型別的範圍。 例如,使用程式碼: "value" in x
。這裡的 "value"
是字串string, x
是聯合型別。值為“true”的分支縮小,需要 x 具有可選或必需屬性的型別的值;值為 “false”
的分支縮小,需要具有可選或缺失屬性的型別的值。
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}
另外,可選屬性還將存在於縮小的兩側,例如,人類可以游泳和飛行(使用正確的裝置),因此應該出 現在 in
檢查的兩側:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
// animal: Fish | Human
animal;
} else {
// animal: Bird | Human
animal;
}
}
4.5 instanceof
操作符縮小
JS有一個運算子instanceof
檢查一個值是否是另一個值的“例項”。更具體地,在JavaScript 中 x instanceof Foo
檢查 x
的原型鏈是否含有 Foo.prototype
。雖然我們不會在這裡深入探討,當 我們進入 類(class)
學習時,你會看到更多這樣的內容,它們大多數可以使用 new
關鍵字例項化。 正如你可能已經猜到的那樣, instanceof
也是一個型別保護,TypeScript 在由 instanceof
保護的分支中實現縮小。
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
} else {
console.log(x.toUpperCase());
}
}
logValue(new Date()) // Mon, 15 Nov 2021 22:34:37 GMT
logValue('hello ts') // HELLO TS
4.6 分配縮小
正如我們之前所提到的, 當我們為任何變數賦值時, TS會檢查賦值的右側並適當縮小左側.
// let x: string | number
let x = Math.random() < 0.5 ? 10 : "hello world!";
x = 1;
// let x: number
console.log(x);
x = "goodbye!";
// let x: string
console.log(x);
請注意,這些分配中的每一個都是有效的。即使在我們第一次賦值後觀察到的型別 x 更改為 number
, 我們仍然可以將 string
賦值給 x 。這是因為宣告型別 x 開始是 string | number
。
如果我們分配了一個 boolean
給 x ,我們就會看到一個錯誤,因為它不是宣告型別的一部分。
let x = Math.random() < 0.5 ? 10 : "hello world!";
// let x: string | number
x = 1;
// let x: number
console.log(x);
// 出錯了~!
x = true
// let x: string | number
console.log(x);
4.7 控制流分析
到目前為止, 我們已經通過一些基本例項來說明TS如何在特定分支中縮小範圍. 但是除了從每個變數中走出來, 並在if
、while
條件等中尋找型別保護之外, 還有更多的事情要做, 比如:
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
}
return padding + input;
}
padLeft從其第一個if塊中返回. TS能夠分析這段程式碼,並看到在padding是數字的情況下, 主體的其餘部分( return padding + input;
)是不可達的。因此,它能夠將數字從 padding
的型別中移除(從string|number縮小到string),用於該函式的其餘部分。
這種基於可達性的程式碼分析被稱為控制流分析,TypeScript使用這種流分析來縮小型別,因為它遇到了 型別守衛和賦值。當一個變數被分析時,控制流可以一次又一次地分裂和重新合併,該變數可以被觀察到在每個點上有不同的型別.
function example() {
let x: string | number | boolean;
x = Math.random() < 0.5;
// let x: boolean
console.log(x);
if (Math.random() < 0.5) {
x = "hello";
// let x: string
console.log(x);
} else {
x = 100;
// let x: number
console.log(x);
}
// let x: string | number
return x;
}
let x = example()
x = 'hello'
x = 100
x = true // error
4.8 使用型別謂詞
到目前為止,我們已經用現有的JavaScript結構來處理窄化問題,然而有時你想更直接地控制整個程式碼中的型別變化。
為了定義一個使用者定義的型別保護,我們只需要定義一個函式,其返回型別是一個型別謂詞.
type Fish = {
name: string
swim: () => void
}
type Bird = {
name: string
fly: () => void
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined
}
在這個例子中, pet is Fish
是我們的型別謂詞。謂詞的形式是 parameterName is Type
,其中 parameterName 必須是當前函式簽名中的引數名稱, 返回一個boolean, 代表是不是該Type
任何時候 isFish
被呼叫時,如果原始型別是相容的,TypeScript將把該變數縮小到該特定型別。
function getSmallPet(): Fish | Bird {
let fish: Fish = {
name: 'gold fish',
swim: () => {
console.log('fish is swimming.')
}
}
let bird: Bird = {
name: 'sparrow',
fly: () => {
console.log('bird is flying.')
}
}
return Math.random() < 0.5 ? bird : fish
}
// 這裡 pet 的 swim 和 fly 都可以訪問了
let pet = getSmallPet() //
console.log(pet)
if (isFish(pet)) {
pet.swim()
} else {
pet.fly()
}
注意,TypeScript不僅知道 pet
在 if
分支中是一條魚;它還知道在 else
分支中,你沒有一條 Fish
,所以你一定有一隻 Bird
。
你可以使用型別守衛 isFish
來過濾 Fish | Bird
的陣列,獲得 Fish
的陣列。
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()]
const underWater1: Fish[] = zoo.filter(isFish)
console.log(underWater1)
// 或者,等同於
const underWater2: Fish[] = zoo.filter(isFish) as Fish[]
console.log(underWater2)
// 對於更復雜的例子,該謂詞可能需要重複使用
const underWatch3: Fish[] = zoo.filter((pet): pet is Fish => {
if (pet.name === 'frog') {
return false
}
return isFish(pet)
})
4.9 受歧視的unions
到目前為止,我們所看的大多數例子都是圍繞著用簡單的型別(如 string
、 boolean
和 number
)來縮小單個變數。雖然這很常見,但在JavaScript中,大多數時候我們要處理的是稍微複雜的結構。
為了激發靈感,讓我們想象一下,我們正試圖對圓形和方形等形狀進行編碼。圓記錄了它們的半徑,方記錄了它們的邊長。我們將使用一個叫做 kind
的欄位來告訴我們正在處理的是哪種形狀。這裡是定義 Shape 的第一個嘗試。
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
注意,我們使用的是字串字面型別的聯合。 "circle"
和 "square"
分別告訴我們應該把這個形狀 當作一個圓形還是方形。通過使用 "circle" | "square "
而不是string
,我們可以避免拼寫錯誤的問題。
function handleShape(shape: Shape) {
// oops!
if (shape.kind === "rect") {
// ...
}
}
我們可以編寫一個 getArea
函式,根據它處理的是圓形還是方形來應用正確的邏輯。我們首先嚐試處理圓形。
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
}
在strictNullChecks
下,這給了我們一個錯誤——這是很恰當的,因為radius
可能沒有被定義。 但是如果我們對kind
屬性進行適當的檢查呢?
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
}
嗯, TypeScript 仍然不知道該怎麼做。我們遇到了一個問題,即我們對我們的值比型別檢查器知道的更多。我們可以嘗試使用一個非空的斷言 ( radius
後面的那個歎號 !
) 來說明 radius
肯定存在。
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius! ** 2;
}
}
但這感覺並不理想。我們不得不用那些非空的斷言對型別檢查器宣告一個歎號(!
),以說服它相信 shape.radius
是被定義的,但是如果我們開始移動程式碼,這些斷言就容易出錯。此外,在 strictNullChecks
之外,我們也可以意外地訪問這些欄位(因為在讀取這些欄位時,可選屬性被認為總是存在的)。我們絕對可以做得更好.
Shape 的這種編碼的問題是,型別檢查器沒有辦法根據種類屬性知道 radius
或 sideLength
是否存在。我們需要把我們知道的東西傳達給型別檢查器。考慮到這一點,讓我們再來定義一下Shape
.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
在這裡,我們正確地將 Shape 分成了兩種型別,為 kind
屬性設定了不同的值,但是 radius
和 sideLength
在它們各自的型別中被宣告為必需的屬性。
讓我們看看當我們試圖訪問 Shape
的半徑時會發生什麼。
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
}
就像我們對 Shape 的第一個定義一樣,這仍然是一個錯誤。當半徑是可選的時候,我們得到了一個錯誤(僅在 strictNullChecks
中),因為 TypeScript 無法判斷該屬性是否存在。現在 Shape 是一個聯合體,TypeScript 告訴我們 shape
可能是一個 Square
,而Square並沒有定義半徑 radius
。 這兩種解釋都是正確的,但只有我們對 Shape 的新編碼仍然在 strictNullChecks
之外導致錯誤.
但是, 如果我們在此嘗試檢查kind屬性呢?
function getArea(shape: Shape) {
if (shape.kind === "circle") {
// shape: Circle
return Math.PI * shape.radius ** 2;
}
}
這就擺脫了錯誤! 當 union
中的每個型別都包含一個與字面型別相同的屬性時,TypeScript 認為這是一 個有區別的 union
,並且可以縮小 union
的成員。
在這種情況下, kind
就是那個共同屬性(這就是 Shape 的判別屬性)。檢查 kind
屬性是否為 "circle"
,就可以剔除 Shape
中所有沒有 "circle"
型別屬性的型別。這就把 Shape
的範圍縮小到 了 Circle
這個型別。
同樣的檢查方法也適用於 switch
語句。現在我們可以試著編寫完整的 getArea
,而不需要任何討厭 的歎號 !
非空的斷言。
function getArea(shape: Shape) {
switch (shape.kind) {
// shape: Circle
case "circle":
return Math.PI * shape.radius ** 2;
// shape: Square
case "square":
return shape.sideLength ** 2;
}
}
這裡最重要的是 Shape
的編碼。向 TypeScript 傳達正確的資訊是至關重要的,這個資訊就是 Circle
和 Square
實際上是具有特定種類欄位的兩個獨立型別。這樣做讓我們寫出型別安全的TypeScript程式碼, 看起來與我們本來要寫的JavaScript沒有區別。從那裡,型別系統能夠做 "正確 "的事情,並找出我們 switch
語句的每個分支中的型別.
辨證的聯合體不僅僅適用於談論圓形和方形。它們適合於在JavaScript中表示任何型別的訊息傳遞方案, 比如在網路上傳送訊息( client/server
通訊),或者在狀態管理框架中編碼突變.
4.10 never
型別與窮盡性檢查
在縮小範圍時,你可以將一個聯合體的選項減少到你已經刪除了所有的可能性並且什麼都不剩的程度。 在這些情況下,TypeScript將使用一個 never
型別來代表一個不應該存在的狀態。
never
型別可以分配給每個型別;但是,沒有任何型別可以分配給never
(除了never本身)。這意味著你可以使用縮小並依靠 never
的出現在 switch
語句中做詳盡的檢查。
例如,在我們的 getArea
函式中新增一個預設值,試圖將形狀分配給 never
,當每個可能的情況都沒有被處理時,就會引發。
type Shape = Circle | Square;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
在 Shape
聯盟中新增一個新成員,將導致TypeScript錯誤
interface Triangle {
kind: "triangle";
sideLength: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
TypeScript學習第五章: 函式
函式是任何應用程式的基本構件,無論它們是本地函式,從另一個模組匯入,還是一個類上的方法。它們也是值,就像其他值一樣,TypeScript有很多方法來描述如何呼叫函式。讓我們來學習一下如何編寫描述函式的型別。
5.1 函式型別表示式
描述一個函式的最簡單是用一個函式型別表示式. 這些型別在語法上類似於箭頭函式.
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
語法 (a: string) => void
意味著有一個引數的函式,名為 a
,型別為字串,沒有返回值"。就像函式宣告一樣,如果沒有指定引數型別,它就隱含為 any
型別。
當然, 我們可以用一個類型別名來命名一個函式型別.
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
5.2 呼叫簽名: 屬性簽名
在JavaScript中,除了可呼叫之外,函式還可以有屬性。然而,函式型別表示式的語法不允許宣告屬性。 如果我們想用屬性來描述可呼叫的東西,我們可以在一個類型別名中寫一個呼叫簽名。
值得注意的是, 類型別名中縮寫的函式型別表示式返回值是用冒號:而不是箭頭函式=>, 且實際應用時所傳入函式返回值必須與此函式型別表示式宣告的相同(fn1和fn2), 如果函式體內沒有操作引數的行為, 可以不傳引數(比如fn3).
type DescribableFunction = { // 物件型別
description: string // 函式的屬性簽名
(someArg: number): boolean // 函式型別表示式, 不能用=> 而是用:
}
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6))
}
// 傳入正常引數使用且正常返回值
function fn1(n: number) {
console.log(n)
return true
}
fn1.description = "hello"
// 不傳入引數且不正常返回值
function fn2() {
console.log("lalala")
}
fn2.description = "heihei"
// 不傳入引數且正常返回值
function fn3() {
return false
}
fn3.description = "hehehe"
doSomething(fn1) // 正常
doSomething(fn2) // 報錯
doSomething(fn3) // 正常
5.3 構造簽名 new (params, ...): Ctor
JS函式也可以用new
操作符來呼叫. TS將這些成為建構函式, 因為它們通常會建立一個新的物件。你可以通過在呼叫簽名前面新增 new
關鍵字來寫一個構造簽名, 返回的是一個類或者建構函式.
class Ctor {
s: string
constructor(s: string) {
this.s = s
}
}
type SomeConstructor = { // 在呼叫簽名前加new就是構造簽名
new (s: string): Ctor // 返回的是一個建構函式或者類
}
function fn(ctor: SomeConstructor) { // SomeConstructor可以理解為建構函式
return new ctor("hello")
}
const f = fn(Ctor)
console.log(f.s)
有些物件,如 JavaScript 的 Date
物件,可以在有 new
或沒有 new
的情況下被呼叫。你可以在同一型別中任意地結合呼叫和構造簽名.
interface CallOrConstruct {
new (s: string): Date
(): string
}
function fn(date: CallOrConstruct) {
let d = new date('2021-11-20')
let n = date() // 因為Date可以在不使用new的情況下呼叫所以程式碼正常
console.log(d)
console.log(n)
}
fn(Date)
下一個例項
// clock建構函式的介面, 是一個構造簽名, 返回一個ClockInterface類的建構函式
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
// Clock類的介面, 裡面有一個tick()函式
interface ClockInterface {
tick(): void;
}
// 建立Clock類的函式
function createClock(
ctor: ClockConstructor,
hour: number,
minute: number
): ClockInterface {
return new ctor(hour, minute);
}
// 具體的類來實現ClockInterface, 必須要有tick函式
class DigitalClock implements ClockInterface {
h: number
m: number
constructor(h: number, m: number) {
this.h = h
this.m = m
}
tick() {
console.log("beep beep");
}
}
// 具體的類來實現ClockInterface, 必須要有tick函式
class AnalogClock implements ClockInterface {
h: number
m: number
constructor(h: number, m: number) {
this.h = h
this.m = m
}
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
console.log(digital)
analog.tick()
5.4 泛型函式<Type>
在寫一個函式時, 輸入的型別與輸出的型別有關, 或者兩個輸入的型別以某種方式相關, 這是常見的. 讓我們考慮一下一個返回陣列種第一個元素的函式.
function firstElement(arr: any[]) {
return arr[0]
}
這個函式完成了它的工作,但不幸的是它的返回型別是 any
。如果該函式返回陣列元素的型別會更好。
在TypeScript中,當我們想描述兩個值之間的對應關係時,會使用泛型。我們通過在函式簽名中宣告一個型別引數來做到這一點:
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0]
}
通過給這個函式新增一個型別引數 Type
,並在兩個地方使用它,我們已經在函式的輸入(陣列)和輸出(返回值)之間建立了一個聯絡。現在當我們呼叫它時,一個更具體的型別就出來了:
// s 是 'string' 型別
const s = firstElement(["a", "b", "c"]);
// n 是 'number' 型別
const n = firstElement([1, 2, 3]);
// u 是 undefined 型別
const u = firstElement([]);
5.4.1 型別推斷
請注意, 在這個例子中, 我們沒有必要指定型別. 型別是由TS推斷出來的------自動選擇.
我們也可以使用多個型別引數. 例如, 一個獨立版本的map看起來可能是這樣的:
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func)
}
// 引數'n'是'字串'型別。
// 'parsed'是'number[]'型別。
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
請注意,在這個例子中,TypeScript可以推斷出輸入型別引數的型別(從給定的字串陣列string),以及基於函式表示式的返回值(數字number)的輸出型別引數。
5.4.2 限制條件
我們i經寫了一些通用函式, 可以對任何型別的值進行操作. 有時我們想把兩個值聯絡起來, 但只能對某個值的子集進行操作. 這種在這種情況下,我們可以使用一個約束條件來限制一個型別引數可以接受的型別。
讓我們寫一個函式,返回兩個值中較長的值。要做到這一點,我們需要一個長度屬性,是一個數字。我們通過寫一個擴充套件子句將型別引數限制在這個型別上.
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
// longerArray 的型別是 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString 是 'alice'|'bob' 的型別。
const longerString = longest("alice", "bob");
// 錯誤! 數字沒有'長度'屬性
const notOK = longest(10, 100);
在這個例子中,有一些有趣的事情需要注意。我們允許TypeScript推斷 longest
的返回型別。返回型別推斷也適用於通用函式。
因為我們將 Type 約束為 { length: number }
,所以我們被允許訪問 a 和 b 引數的 .length
屬 性。如果沒有型別約束,我們就不能訪問這些屬性,因為這些值可能是一些沒有長度屬性的其他型別。
longerArray 和 longerString 的型別是根據引數推斷出來的。記住,泛型就是把兩個或多個具有相同型別的值聯絡起來。 最後,正如我們所希望的,對 longest(10, 100) 的呼叫被拒絕了,因為數字型別沒有一個 .length
屬性
5.4.3 使用受限值
這裡有一個使用通用約束條件時的常見錯誤。
function minimumLength<Type extends { length: number }>(
obj: Type,
minimum: number
): Type {
if (obj.length >= minimum) {
return obj
} else {
return { length: minimum }
}
}
看起來這個函式沒有問題--Type被限制為{ length: number }
,而且這個函式要麼返回Type,要麼返回一 個與該限制相匹配的值。問題是,該函式承諾返回與傳入的物件相同的型別,而不僅僅是與約束條件相匹配的一些物件。如果這段程式碼是合法的,你可以寫出肯定無法工作的程式碼。
// 'arr' 獲得值: { length: 6 }
const arr = minimumLength([1, 2, 3], 6);
//在此崩潰,因為陣列有一個'切片'方法,但沒有返回物件!
console.log(arr.slice(0));
5.4.4 指定型別引數
TypeScript 通常可以推斷出通用呼叫中的預期型別引數,但並非總是如此。例如,假設你寫了一個函式來合併兩個陣列:
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2)
}
通常情況下,用不匹配的陣列呼叫這個函式是一個錯誤:
const arr = combine([1, 2, 3], ["hello"]);
然而,如果你打算這樣做,你在呼叫函式時可以手動指定型別:
const arr = combine<string | number>([1, 2, 3], ["hello"])
5.4.5 編寫優秀通用函式的準則
編寫泛型函式很有趣,而且很容易被型別引數所迷惑。有太多的型別引數或在不需要的地方使用約束,會使推理不那麼成功,使你的函式的呼叫者感到沮喪。
- 型別引數下推
下面是兩種看似的函式寫法:
function firstElement1<Type>(arr: Type[]) {
return arr[0];
}
function firstElement2<Type extends any[]>(arr: Type) {
return arr[0];
}
// a: number (推薦)
const a = firstElement1([1, 2, 3]);
// b: any (不推薦)
const b = firstElement2([1, 2, 3]);
乍一看,這些可能是相同的,但 firstElement1
是寫這個函式的一個更好的方法。它的推斷返回型別是Type
,但 firstElement2
的推斷返回型別是 any
,因為TypeScript必須使用約束型別來解析arr[0]
表示式,而不是在呼叫期間 "等待 "解析該元素。
規則: 在可能的情況下, 使用型別引數本身, 而不是對其進行約束
- 使用更少的型別引數
下面是另一對類似的函式:
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func);
}
function filter2<Type, Func extends (arg: Type) => boolean>(
arr: Type[],
func: Func
): Type[] {
return arr.filter(func);
}
我們已經建立了一個型別引數 Func
,它並不涉及兩個值。這總是一個值得標記的壞習慣,因為它意味著想要指定型別引數的呼叫者必須無緣無故地手動指定一個額外的型別引數。 Func 除了使函式更難閱讀和推理外,什麼也沒做。
規則: 總是儘可能少的使用型別引數
- 型別引數應該出現兩次及以上
有時候我們會忘記, 一個函式可能不需要是通用的:
function greet<Str extends string>(s: Str) {
console.log("Hello, " + s);
}
greet("world");
我們完全可以寫一個更簡單的版本:
function greet(s: string) {
console.log("Hello, " + s);
}
記住,型別引數是用來關聯多個值的型別的。如果一個型別引數在函式簽名中只使用一次,那麼它就沒有任何關係。
規則: 如果一個型別的引數只出現在一個地方, 請重新考慮你是否真的需要它
5.5 可選引數 ?
JavaScript中的函式經常需要一個可變數量的引數。例如,number
的 toFixed
方法需要一個可選的數字計數。
function f(n: number) {
console.log(n.toFixed()); // 0 個引數
console.log(n.toFixed(3)); // 1 個引數
}
我們可以在TypeScript中通過將引數用 ?
標記:
function f(x?: number) {
// ...
}
f(); // 正確
f(10); // 正確
雖然引數被指定為 number 型別,但 x
引數實際上將具有 number | undefined
型別,因為在 JavaScript中未指定的引數會得到 undefined
的值。
你也可以提供一個引數預設值
function f(x = 10) {
//...
}
現在在 f
的主體中, x 將具有 number
型別,因為任何 undefined
的引數將被替換為10 。請注意,當一個引數是可選的,呼叫者總是可以傳遞未定義的引數,因為這只是模擬一個 "丟失 "的引數:
declare function f(x?: number): void;
// 以下呼叫都是正確的
f();
f(10);
f(undefined);
5.5.1 回撥中的可選引數
一旦你瞭解了可選引數和函式型別表示式, 在編寫呼叫回撥的函式時就很容易犯以下錯誤:
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}
我們在寫index?
作為一個可選引數時, 通常是想讓這些呼叫都是合法的:
myForEach([1, 2, 3], (a) => console.log(a))
myForEach([1, 2, 3], (a, i) => console.log(a, i))
這實際上意味著回撥可能會被呼叫,只有一個引數。換句話說,該函式定義說,實現可能是這樣的:
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
// 我現在不想提供索引
callback(arr[i]);
}
}
反過來,TypeScript會強制執行這個意思,併發出實際上不可能的錯誤:
myForEach([1, 2, 3], (a, i) => {
console.log(i.toFixed())
})
在JavaScript中,如果你呼叫一個形參多於實參的函式,額外的引數會被簡單地忽略。TypeScript的行為也是如此。
引數較少的函式(相同的型別)總是可以取代引數較多的函式的位置。
當為回撥寫一個函式型別時, 永遠不要寫一個可選引數, 除非你打算在不傳遞該引數的情況下呼叫函式.
5.6 函式過載: 過載簽名
一些 JavaScript 函式可以在不同的引數數量和型別中被呼叫。例如,你可能會寫一個函式來產生一個 Date
,它需要一個時間戳(一個引數)或一個月/日/年規格(三個引數)。
在TypeScript中,我們可以通過編寫過載簽名來指定一個可以以不同方式呼叫的函式。要做到這一點, 要寫一些數量的函式簽名(通常是兩個或更多),然後是函式的主體:
function makeDate(timestamp: number): Date // 過載簽名
function makeDate(m: number, d: number, y: number): Date // 過載簽名
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if(d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d)
} else {
return new Date(mOrTimestamp)
}
}
const d1 = makeDate(12345678)
const d2 = makeDate(5,5,5)
const d3 = makeDate(1, 3)
在這個例子中,我們寫了兩個過載:一個接受一個引數,另一個接受三個引數。這前兩個簽名被稱為過載簽名。
然後,我們寫了一個具有相容簽名的函式實現。函式有一個實現簽名,但這個簽名不能被直接呼叫。即使我們寫了一個在所需引數之後有兩個可選引數的函式,它也不能以兩個引數被呼叫
5.6.1 過載簽名和實現簽名
這是一個常簡的混亂的來源. 通常我們會寫這樣的程式碼, 卻不明白為什麼會出現錯誤:
function fn(x: string): void
function fn() {
// ...
}
// 期望能夠以零參呼叫
fn()
同樣, 用於編寫函式體的簽名不能從外面"看到":
實現的簽名從外面是看不到的. 在編寫過載函式時, 你應該總是在函式的實現上面有兩個或多個簽名.
實現簽名也必須與過載簽名相容. 例如, 這些函式有錯誤, 因為實現簽名沒有以正確的方式匹配過載:
function fn(x: boolean): void;
// 引數型別不正確
function fn(x: string): void;
function fn(x: boolean) {}
function fn(x: string): string
// 返回型別不正確
function fn(x: number): boolean
function fn(x: string | number) {
return "oops";
}
5.6.2 編寫好的過載
和泛型一樣,在使用函式過載時,有一些準則是你應該遵循的。遵循這些原則將使你的函式更容易呼叫,更容易理解,更容易實現。
讓我們考慮一個返回字串或陣列長度的函式:
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
return x.length;
}
這個函式是好的;我們可以用字串或陣列來呼叫它。然而,我們不能用一個可能是字串或陣列的值來呼叫它,因為TypeScript只能將一個函式呼叫解析為一個過載:
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);
因為兩個過載都有相同的引數數量和相同的返回型別,我們可以改寫一個非過載版本的函式:
function len(x: any[] | string) {
return x.length;
}
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]); // OK
這就好得多了! 呼叫者可以用任何一種值來呼叫它,而且作為額外的獎勵,我們不需要找出一個正確的實現簽名。
在可能的情況下,總是傾向於使用聯合型別的引數而不是過載引數
5.6.3 函式內This
的宣告
TS會通過程式碼分析來推斷函式中this
應該是什麼, 比如下面的例子:
const user = {
id: 123,
admin: false,
becomeAdmin: function () {
this.admin = true;
},
};
TS理解函式user.becomeAdmin
有一個對應的this
, 它是外部物件user
. 這個對於很多情況來說已經足夠了, 但是有很多情況下你需要更多的控制this
代表什麼物件/
JavaScript規範規定, 你不能有一個叫 this
的引數,所以TypeScript使用這個語法空間,讓你在函式體中宣告 this
的型別。
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// NOTE: The function now explicitly specifies that its callee must be of type Deck
createCardPicker: function(this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
console.log(deck.createCardPicker()())
5.7 需要了解的其他型別
有一些額外的型別你會想要認識,它們在處理函式型別時經常出現。像所有的型別一樣,你可以在任何地方使用它們,但這些型別在函式的上下文中特別相關.
5.7.1 void
void
表示沒有返回值的函式的返回值. 當一個函式沒有任何返回語句, 或者沒有從這些返回語句中返回任何明確的值時, 它都是推斷出來void
型別.
// 推斷出的返回型別是void
function noop() {
return;
}
在JavaScript中,一個不返回任何值的函式將隱含地返回 undefinded
的值。然而,在TypeScript中, void 和 undefined
是不一樣的。在本章末尾有進一步的細節。
void 和 undefined是不一樣的
5.7.2 object
特殊型別object
指的是任何不是基元的值( string
、 number
、 bigint
、 boolean
、 symbol
、 null
或 undefined
)。這與空物件型別 { } 不同,也與全域性型別 Object
不同。你很可能永遠不會使用 Object
。
object
不是Object
。始終使用object
!
請注意,在JavaScript中,函式值是物件。它們有屬性,在它們的原型鏈中有 Object.prototype ,是 Object 的例項,你可以對它們呼叫 Object.key ,等等。由於這個原因,函式型別在TypeScript中被 認為是 object 。
5.7.3 unknown
unknown
型別代表任何值. 這與any
型別相似, 但更安全, 因為對未知unknown
值做任何事情都是不合法的.
function f1(a: any) {
a.b(); // 正確
}
function f2(a: unknown) {
a.b();
}
這在描述函式型別時很有用,因為你可以描述接受任何值的函式,而不需要在函式體中有 any 值。 反之,你可以描述一個返回未知型別的值的函式:
function safeParse(s: string): unknown {
return JSON.parse(s);
}
// 需要小心對待'obj'!
const obj = safeParse(someRandomString);
5.7.4 never
有些函式永遠不會返回一個值:
function fail(msg: string): never {
throw new Error(msg);
}
never
型別標識永遠不會被觀察到的值. 載一個返回型別中, 這意味著函式丟擲了一個異常或終止程式的執行.
never 也出現在TypeScript確定一個 union
中沒有任何東西的時候。
function fn(x: string | number) {
if (typeof x === "string") {
// 做一些事
} else if (typeof x === "number") {
// 再做一些事
} else {
x; // 'never'!
}
5.7.5 Function
全域性性的 Function
型別描述了諸如 bind
、 call
、 apply
和其他存在於JavaScript中所有函式值的屬性。它還有一個特殊的屬性,即 Function
型別的值總是可以被呼叫;這些呼叫返回 any
。
function doSomething(f: Function) {
return f(1, 2, 3);
}
這是一個無型別的函式呼叫,一般來說最好避免,因為 any 返回型別都不安全。 如果你需要接受一個任意的函式,但不打算呼叫它,一般來說, () => void
的型別比較安全。
5.8 函式展開運算子
5.8.1 形參展開(Rest Parameters)
除了使用可選引數或過載來製作可以接受各種固定引數數量的函式之外,我們還可以使用休止引數來定義接受無限制數量的引數的函式。
rest
引數出現在所有其他引數之後,並使用 ...
的語法:
function multiply(n: number, ...m: number[]) {
return m.map((x) => n * x);
}
// 'a' 獲得的值 [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);
在TypeScript中,這些引數的型別註解是隱含的 any[]
,而不是 any
,任何給出的型別註解必須是 Array
或 T[]
的形式,或一個元組型別(我們將在後面學習).
5.8.2 實參展開(Rest Arguments)
反之, 我們可以使用spread
語法從陣列中提供可變數量的引數. 例如陣列的push
方法需要任意數量的引數.
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);
console.log(arr1) //[ 1, 2, 3, 4, 5, 6 ]
請注意,一般來說,TypeScript並不假定陣列是不可變的。這可能會導致一些令人驚訝的行為。
// 推斷的型別是 number[] -- "一個有零或多個數字的陣列"。
// 不專指兩個數字
const args = [8, 5];
const angle = Math.atan2(...args);
這種情況的最佳解決方案取決於你的程式碼,但一般來說, const context
是最直接的解決方案
// 推斷為2個長度的元組
const args = [8, 5] as const;
// 正確
const angle = Math.atan2(...args);
5.9 引數解構
可以使用引數重構來方便地將作為引數提供的物件,解壓到函式主體的一個或多個區域性變數中。在 JavaScript中,它看起來像這樣:
function sum({ a, b, c }) {
console.log(a + b + c);
}
sum({ a: 10, b: 3, c: 9 });
物件的型別註解在結構的語法之後:
function sum( { a, b, c }: { a: number, b: number, c: number }) {
console.log(a + b + c)
}
這看起來有點囉嗦,但你也可以在這裡使用一個命名的型別:
// 與之前的例子相同
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
console.log(a + b + c);
}
5.10 函式的可分配性: 返回void
型別
函式的 void
返回型別可以產生一些不尋常的,但卻是預期的行為。
返回型別為 void
的上下文型別並不強迫函式不返回東西。另一種說法是,一個具有 void
返回型別的上下文函式型別( type vf = () => void
),在實現時,可以返回任何其他的值,但它會被忽略。
因此,以下 () => void
型別的實現是有效的:
type voidFunc = () => void
const f1: voidFunc = () => {
return true
}
const f2: voidFunc = () => true
const f3: voidFunc = function () {
return true
}
而當這些函式之一的返回值被分配給另一個變數時,它將保留 void
的型別
const v1 = f1();
const v2 = f2();
const v3 = f3();
console.log(v1) // true
console.log(v2) // true
console.log(v3) // true
這種行為的存在使得下面的程式碼是有效的,即使 Array.prototype.push
返回一個數字,而Array.prototype.forEach
方法期望一個返回型別為 void
的函式:
const src = [1, 2, 3];
const dst = [0];
src.forEach((el) => dst.push(el));
還有一個需要注意的特殊情況,當一個字面的函式定義有一個 void
的返回型別時,該函式必須不返回任何東西。
function f2(): void {
return true;
}
const f3 = function (): void {
return true;
}
TypeScript學習第六章: 物件型別
在JavaScript中,我們分組和傳遞資料的基本方式是通過物件。在TypeScript中,我們通過物件型別來表示這些物件。
正如我們所見,它們可以是匿名的:
function greet(person: { name: string; age: number }) { // 匿名物件{ name: string; age: number }
return "Hello " + person.name;
}
或者可以通過使用一個介面來命名它們:
interface Person { // 介面中定義了一個物件型別,包含name和age
name: string
age: number
}
function greet(person: Person) {
return 'Hello ' + person.name
}
或者類型別名
type Person = { // 類型別名種定義了一個物件型別, 其包含name和age
name: string;
age: number;
};
function greet(person: Person) {
return "Hello " + person.name;
}
在上面的三個例子中,我們寫了一些函式,這些函式接收包含屬性 name
(必須是一個 string
)和 age
(必須是一個 number
)的物件.
6.1 屬性修改器
物件型別中的每個屬性都可以指定幾件事:型別、屬性是否是可選的,以及屬性是否可以被寫入。
6.2 可選屬性
很多時候,我們會發現自己處理的物件可能有一個屬性設定。在這些情況下,我們可以在這些屬性的名 字後面加上一個問號(?
),把它們標記為可選的
type Shape = {}
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
function paintShape(opts: PaintOptions) {
// ...
}
const shape: Shape = {}
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });
在這個例子中, xPos
和 yPos
都被認為是可選的。我們可以選擇提供它們中的任何一個,所以上面對 paintShape
的每個呼叫都是有效的。所有的可選性實際上是說,如果屬性被設定,它最好有一個特定 的型別。
我們也可以從這些屬性中讀取,但當我們在 strictNullChecks 下讀取時,TypeScript會告訴我們它們可能是未定義的。因為未賦值時值為undefined
.
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos;
let yPos = opts.yPos;
// ...
}
在JavaScript中,即使該屬性從未被設定過,我們仍然可以訪問它--它只是會給我們未定義的值。我們可以專門處理未定義。
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos === undefined ? 0 : opts.xPos;
let yPos = opts.yPos === undefined ? 0 : opts.yPos;
// ...
}
請注意,這種為未指定的值設定預設值的模式非常普遍,以至於JavaScript有語法來支援它。
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {// 注意, 此時用瞭解構的語法, 將PaintOptions裡的引數結構出來, 並給xPos和yPos設定了預設值
console.log("x coordinate at", xPos);
console.log("y coordinate at", yPos);
// ...
}
在這裡,我們為 paintShape 的引數使用了一個解構模式,併為 xPos
和 yPos
提供了預設值。現在 xPos
和 yPos
都肯定存在於 paintShape
的主體中,但對於 paintShape 的任何呼叫者來說是可選 的。
請注意,目前還沒有辦法將型別註釋放在解構模式中。這是因為下面的語法在JavaScript中已經有了不同的含義。
function redner(args: Shape | number) {}
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
render(shape);
render(xPos);
}
在一個物件解構模式中, shape: Shape
意味著 "獲取屬性 shape
,並在本地重新定義為一個名為 Shape
的變數。同樣, xPos: number
建立一個名為number
的變數,其值基於引數的 xPos
。
6.3 只讀屬性
對於TypeScript,屬性也可以被標記為只讀。雖然它不會在執行時改變任何行為,但在型別檢查期間, 可以在一個屬性前加readonly
一個標記為只讀的屬性不能被寫入.
interface SomeType {
readonly prop: string;
}
function doSomething(obj: SomeType) {
// 可以讀取 'obj.prop'.
console.log(`prop has the value '${obj.prop}'.`);
// 但不能重新設定值
obj.prop = "hello";
}
使用 readonly
修飾符並不一定意味著一個值是完全不可改變的。或者換句話說,它的內部內容不能被 改變,它只是意味著該屬性本身不能被重新寫入。
interface Home {
readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
// 我們可以從'home.resident'讀取和更新屬性。
console.log(`Happy birthday ${home.resident.name}!`);
home.resident.age++;
}
function evict(home: Home) {
// 但是我們不能寫到'home'上的'resident'屬性本身。
home.resident = {
name: "Victor the Evictor",
age: 42,
};
}
管理對 readonly
含義的預期是很重要的。在TypeScript的開發過程中,對於一個物件應該如何被使用 的問題,它是有用的訊號。TypeScript在檢查兩個型別的屬性是否相容時,並不考慮這些型別的屬性是 否是 readonly
,所以 readony
屬性也可以通過別名來改變.
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
// 正常工作
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // 列印 '42'
writablePerson.age++;
console.log(readonlyPerson.age); // 列印 '43'
6.4 索引簽名
有時你並不提前知道一個型別的所有屬性名稱,但你知道值的型別。
在這些情況下,你可以使用一個索引簽名來描述可能的值的型別,比如說:
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = ['a', 'b'];
const secondItem = myArray[1];
上面,我們有一個 StringArray
介面,它有一個索引簽名。這個索引簽名指出,當一個 StringArray
被數字索引時,它將返回一個字串。
索引簽名的屬性型別必須是 string
或 number
。
支援兩種型別的索引器是可能的,但是從數字索引器返回的型別必須是字串索引器返回的型別的子型別。這是因為當用 "數字 "進行索引時,JavaScript實際上在索引到一個物件之前將其轉換為 "字串"。這意味著用 100 (一個數字)進行索引和用 "100" (一個字串)進行索引是一樣的,所以兩者需要一致。
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
雖然字串索引簽名是描述 "字典 "模式的一種強大方式,但它也強制要求所有的屬性與它們的返回型別相匹配。這是因為字串索引宣告 obj.property
也可以作為 obj["property"]
。在下面的例子中, name
的型別與字串索引的型別不匹配,型別檢查器會給出一個錯誤:
interface NumberDictionary {
[index: string]: number;
length: number; // ok
name: string; // error
}
然而,如果索引簽名是屬性型別的聯合,不同型別的屬性是可以接受的:
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // 正確, length 是 number 型別
name: string; // 正確, name 是 string 型別
}
最後,你可以使索引簽名為只讀,以防止對其索引的賦值:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
你不能設定 myArray[2]
,因為這個索引簽名是隻讀的。
6.5 擴充套件型別
有一些型別可能是其他型別的更具體的版本,這是很常見的。例如,我們可能有一個 BasicAddress 類 型,描述傳送信件和包裹所需的欄位。
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
在某些情況下,這就足夠了,但是如果一個地址的小區內有多個單元,那麼地址往往有一個單元號與之 相關。我們就可以描述一個 AddressWithUnit
:
interface AddressWithUnit {
name?: string;
unit: string;
street: string;
city: string;
country: string;
postalCode: string;
}
這就完成了工作,但這裡的缺點是,當我們的變化是純粹的加法時,我們不得不重複 BasicAddress 的 所有其他欄位。相反,我們可以擴充套件原始的 BasicAddress 型別,只需新增 AddressWithUnit 特有的 新欄位:
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}
介面上的 extends
關鍵字,允許我們有效地從其他命名的型別中複製成員,並新增我們想要的任何新成員。這對於減少我們不得不寫的型別宣告模板,以及表明同一屬性的幾個不同宣告可能是相關的意圖來說,是非常有用的。例如, AddressWithUnit 不需要重複 street 屬性,而且因為 street 源於 BasicAddress ,我們會知道這兩種型別在某種程度上是相關的。
介面也可以從多個型別中擴充套件。
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 42,
}
6.6 交叉型別
介面允許我們通過擴充套件其他型別建立起新的型別。TypeScript提供了另一種結構,稱為交叉型別,主要用於組合現有的物件型別。
叉型別是用 &
操作符定義的.
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
const cc: ColorfulCircle = {
color: "red",
radius: 42,
}
在這裡,我們將 Colorful
和 Circle
相交,產生了一個新的型別,它擁有 Colorful
和 Circle
的 所有成員。
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
// 正確
draw({ color: "blue", radius: 42 });
// 錯誤
draw({ color: "red", raidus: 42 });
6.7 介面與交叉型別
我們剛剛看了兩種組合型別的方法,它們很相似,但實際上有細微的不同。對於介面,我們可以使用 extends
子句來擴充套件其他型別,而對於交叉型別,我們也可以做類似的事情,並用類型別名來命名結 果。兩者之間的主要區別在於如何處理衝突,這種區別通常是你在介面和交叉型別的類型別名之間選擇 一個的主要原因之一。
介面可以定義多次, 多次的宣告會自動合併
interface Sister {
name: string
}
interface Sister {
age: number
}
const sisterAn: Sister = {
name: "sisterAn"
}
const sisterRan: Sister = {
name: "sisterRan",
age: 12
}
但是類型別名如果定義多次,會報錯:
type Sister = {
name: string;
}
type Sister = {
age: number;
}
6.8 泛型物件型別
讓我們想象一下,一個可以包含任何數值的盒子型別:字串、數字、長頸鹿,等等.
interface Box {
contents: any;
}
現在,內容屬性的型別是任意,這很有效,但會導致下一步的意外。
我們可以使用 unknown
,但這意味著在我們已經知道內容型別的情況下,我們需要做預防性檢查,或者使用容易出錯的型別斷言。
interface Box {
contents: unknown;
}
let x: Box = {
contents: "hello world",
};
// 我們需要檢查 'x.contents'
if (typeof x.contents === "string") {
console.log(x.contents.toLowerCase());
}
// 或者用型別斷言
console.log((x.contents as string).toLowerCase());
一種安全的方法是為每一種型別的內容搭建不同的盒子型別:
interface NumberBox {
contents: number;
}
interface StringBox {
contents: string;
}
interface BooleanBox {
contents: boolean;
}
但這意味著我們必須建立不同的函式,或函式的過載,以對這些型別進行操作:
function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
box.contents = newContents;
}
那是一個很大的模板。此外,我們以後可能需要引入新的型別和過載。這是令人沮喪的,因為我們的盒 子型別和過載實際上都是一樣的.
相反, 我們可以做一個通用的Box
型別, 宣告一個引數型別:
interface Box<Type> {
contents: Type
}
你可以把這句話理解為:"一個型別的盒子,是它的內容具有型別的東西"。以後,當我們引用 Box
時, 我們必須給一個型別引數來代替 Type
。
let box: Box<string>
把 Box
想象成一個真實型別的模板,其中 Type
是一個佔位符,會被替換成其他型別。當 TypeScript看到 Box<string>
時,它將用字串替換 Box<Type>
中的每個 Type
例項,並最終以 { contents: string }
這樣的方式工作。換句話說, Box
和我們之前的 StringBox
工作起來是一樣的。
interface Box<Type> {
contents: Type;
}
interface StringBox {
contents: string;
}
let boxA: Box<string> = { contents: "hello" };
boxA.contents;
let boxB: StringBox = { contents: "world" };
boxB.contents;
盒子是可重用的,因為Type可以用任何東西來代替。這意味著當我們需要一個新型別的盒子時,我們根 本不需要宣告一個新的盒子型別(儘管如果我們想的話,我們當然可以)。
interface Box<Type> {
contents: Type;
}
interface Apple {
// ....
}
// 等價於 '{ contents: Apple }'.
type AppleBox = Box<Apple>;
這也意味著我們可以完全避免過載,而是使用通用函式。
function setContents<Type>(box: Box<Type>, newContents: Type) {
box.contents = newContents;
}
通過使用一個類型別名來代替:
type Box<Type> = {
contents: Type;
}
由於類型別名與介面不同,它不僅可以描述物件型別,我們還可以用它來編寫其他型別的通用輔助類 型。
type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
我們將在稍後回到類型別名。
通用物件型別通常是某種容器型別,它的工作與它們所包含的元素型別無關。資料結構以這種方式工作是很理想的,這樣它們就可以在不同的資料型別中重複使用。
6.9 陣列型別
我們一直在使用這樣一種型別:陣列型別。每當我們寫出 number[]
或 string[]
這樣的型別時,這 實際上只是 Array<number>
和 Array<string>
的縮寫:
function doSomething(value: Array<string>) {
// ...
}
let myArray: string[] = ["hello", "world"];
// 這兩樣都能用
doSomething(myArray);
doSomething(new Array("hello", "world"));
和上面的 Box 型別一樣, Array
本身也是一個通用型別。
interface Array<Type> {
/**
* 獲取或設定陣列的長度。
*/
length: number;
/**
* 移除陣列中的最後一個元素並返回。
*/
pop(): Type | undefined;
/**
* 向一個數組新增新元素,並返回陣列的新長度。
*/
push(...items: Type[]): number;
// ...
}
現代JavaScript還提供了其他通用的資料結構,比如 Map<K, V>
, Set<T>
, 和 Promise<T>
。這實際上意味著,由於 Map
、 Set
和 Promise
的行為方式,它們可以與任何型別的集合一起工作。
6.10 只讀陣列型別
ReadonlyArray
是一個特殊的型別,描述了不應該被改變的陣列。
function doStuff(values: ReadonlyArray<string>) {
// 我們可以從 'values' 讀資料...
const copy = values.slice();
console.log(`第一個值是 ${values[0]}`);
// ...但我們不能改變 'vulues' 的值。
values.push("hello!");
}
和屬性的 readonly
修飾符一樣,它主要是一個我們可以用來了解意圖的工具。當我們看到一個返回ReadonlyArrays
的函式時,它告訴我們我們根本不打算改變其內容,而當我們看到一個消耗ReadonlyArrays
的函式時,它告訴我們可以將任何陣列傳入該函式,而不用擔心它會改變其內容。
與 Array 不同,沒有一個我們可以使用的 ReadonlyArray
建構函式。
new ReadonlyArray("red", "green", "blue");
相反,我們可以將普通的 Array
分配給 ReadonlyArray
。
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
正如 TypeScript為 Array<Type>
提供了 Type[]
的速記語法一樣,它也為 ReadonlyArray<Type>
提 供了只讀 Type[]
的速記語法。
最後要注意的是,與 readony 屬性修改器不同,可分配性在普通 Array 和 ReadonlyArray 之間不是 雙向的。
let x: readonly string[] = [];
let y: string[] = [];
x = y;
y = x;
6.11 元組型別
Tuple 型別是另一種 Array 型別,它確切地知道包含多少個元素,以及它在特定位置包含哪些型別。
type StringNumberPair = [string, number];
這裡, StringNumberPair
是一個 string
和 number
的元組型別。像 ReadonlyArray
一樣,它在執行時沒有表示,但對TypeScript來說是重要的。對於型別系統來說, StringNumberPair
描述了其 索引 0 包含字串和 索引1 包含數字的陣列。
function doSomething(pair: [string, number]) {
const a = pair[0];
const b = pair[1];
// ...
}
doSomething(["hello", 42])
如果我們試圖索引超過元素的數量,我們會得到一個錯誤:
function doSomething(pair: [string, number]) {
const c = pair[2];
}
我們還可以使用JavaScript的陣列析構來對元組進行解構。
function doSomething(stringHash: [string, number]) {
const [inputString, hash] = stringHash;
console.log(inputString);
console.log(hash);
}
除了這些長度檢查,像這樣的簡單元組型別等同於 Array 的版本,它為特定的索引宣告屬性,並且用數字字面型別宣告長度。
interface StringNumberPair {
// 專有屬性
length: 2;
0: string;
1: number;
// 其他 'Array<string | number>' 成員...
slice(start?: number, end?: number): Array<string | number>;
}
另一件你可能感興趣的事情是,元組可以通過在元素的型別後面寫出問號(?
)—— 可選的元組,元素 只能出現在末尾,而且還影響到長度的型別。
type Either2dOr3d = [number, number, number?];
function setCoordinate(coord: Either2dOr3d) {
const [x, y, z] = coord;
console.log(`提供的座標有 ${coord.length} 個維度`);
}
圖元也可以有其餘元素,這些元素必須是 array
/tuple
型別.
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
-
StringNumberBooleans
描述了一個元組,其前兩個元素分別是字串和數字,但後面可以有任意數量的布林。 -
StringBooleansNumber
描述了一個元組,其第一個元素是字串,然後是任意數量的布林運算,最後是一個數字。 -
BooleansStringNumber
描述了一個元組,其起始元素是任意數量的布林運算,最後是一個字元 串,然後是一個數字。
一個有其餘元素的元組沒有集合的 "長度"——它只有一組不同位置的知名元素。
function readButtonInput(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
console.log(name)
console.log(version)
console.log(input)
// ...
}
const data: boolean[] = [true, false, true]
const args:[string, number, ...boolean[]] = ["Hello world", 100, ...data]
console.log(args)
readButtonInput(...args)
// [ 'Hello world', 100, true, false, true ]
// Hello world
// 100
// [ true, false, true ]
基本上等同於:
function readButtonInput(name: string, version: number, ...input: boolean[]) {
// ...
}
當你想用一個其餘(rest)引數接受可變數量的引數,並且你需要一個最小的元素數量,但你不想引入中間變數時,這很方便。
6.12 只讀元組型別
關於 tuple
型別的最後一點說明: tuple
型別有隻讀特性,可以通過在它們前面貼上一個 readonly
修飾符來指定——就像陣列的速記語法一樣.
function doSomething(pair: readonly [string, number]) {
// ...
}
正如你所期望的,在TypeScript中不允許向只讀元組的任何屬性寫入:
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!";
}
在大多數程式碼中,元組往往被建立並不被修改,所以在可能的情況下,將型別註釋為只讀元組是一個很 好的預設。這一點也很重要,因為帶有 const
斷言的陣列字面量將被推斷為只讀元組型別.
let point = [3, 4] as const;
function distanceFromOrigin([x, y]: [number, number]) {
return Math.sqrt(x ** 2 + y ** 2);
}
distanceFromOrigin(point);
在這裡, distanceFromOrigin
從未修改過它的元素,而是期望一個可變的元組。由於 point
的型別被推斷為只讀的 [3, 4]
,它與 [number, number]
不相容,因為該型別不能保證 point
的元素不被修改。
TypeScript學習第七章: 型別操縱
7.0 從型別中建立型別
TS的型別系統非常強大, 因為它允許使用其他型別的術語來表達型別.
這個想法的最簡單的形式是泛型, 我們實際上有各種各樣的型別操作符可以使用. 也可以用我們已經有的值來表達型別.
通過結合各種型別操作符,我們可以用一種簡潔、可維護的方式來表達複雜的操作和值. 在本節中,我們將介紹用現有的型別或值來表達一個新型別的方法.
- 泛型型 - 帶引數的型別
- Keyof 型別操作符-
keyof
操作符建立新型別 - Typeof 型別操作符 - 使用
typeof
操作符來建立新的型別 - 索引訪問型別 - 使用
Type['a']
語法來訪問一個型別的子集 - 條件型別 - 在型別系統中像
if
語句一樣行事的型別 - 對映型別 - 通過對映現有型別中的每個屬性來建立型別
- 模板字面量型別 - 通過模板字面字串改變屬性的對映型別
7.1 泛型
軟體工程的一個主要部分是建立元件,這些元件不僅有定義明確和一致的API,而且還可以重複使用。能夠處理今天的資料和明天的資料的元件將為你建立大型軟體系統提供最靈活的能力。
泛型能夠建立一個在各種型別上工作的元件,而不是單一的型別。這使得使用者可以消費這些元件並使用他們自己的型別。
7.1.1 Hello World
首先, 讓我們做一下泛型的"Hello World": 身份函式. 身份函式使用個函式, 他將返回傳入的任何內容. 你一用類似於echo命令的方式來考慮它.
如果沒有泛型, 我們將不得不給身份函式一個特定的型別.
function echo(arg: number): number {
return arg
}
或者,我們可以用任意型別來描述身份函式:
function echo(arg: any): any {
return arg
}
使用 any
當然是通用的,因為它將使函式接受 arg
型別的任何和所有的型別,但實際上我們在函式返回時失去了關於該型別的資訊。如果我們傳入一個數字,我們唯一的資訊就是任何型別都可以被返回。
相反,我們需要一種方法來捕獲引數的型別,以便我們也可以用它來表示返回的內容。在這裡,我們將使用一個型別變數,這是一種特殊的變數,對型別而不是數值起作用。
function echo<Type>(arg: Type): Type {
return arg
}
我們現在已經在身份函式中添加了一個型別變數 Type
。這個 Type
允許我們捕獲使用者提供的型別(例如數字),這樣我們就可以在以後使用這些資訊。這裡,我們再次使用Type
作為返回型別。經過檢查, 我們現在可以看到引數和返回型別使用的是相同的型別。這使得我們可以將型別資訊從函式的一側輸入,然後從另一側輸出。
我們說這個版本的身份函式是通用的,因為它在一系列的型別上工作。與使用任何型別不同的是,它也和第一個使用數字作為引數和返回型別的身份函式一樣精確(即,它不會丟失任何資訊)。
一旦我們寫好了通用身份函式,我們就可以用兩種方式之一來呼叫它。第一種方式是將所有的引數,包括型別引數,都傳遞給函式:
let output = echo<string>("myString")
這裡我們明確地將 Type
設定為 string
,作為函式呼叫的引數之一,用引數周圍的 <>
而不是 ()
來表示。
第二種方式可能也是最常見的。這裡我們使用型別引數推理——也就是說,我們希望編譯器根據我們傳入的引數的型別,自動為我們設定 Type
的值。
let output = echo("myString")
注意,我們不必在角括號(<>
)中明確地傳遞型別;編譯器只是查看了 "myString "
這個值,並將Type
設定為其型別。雖然型別引數推斷是一個有用的工具,可以使程式碼更短、更易讀,但當編譯器不能推斷出型別時,你可能需要像我們在前面的例子中那樣明確地傳入型別引數,這在更復雜的例子中可能發生。
7.1.2 使用通用型別變數
當你開始使用泛型時,你會注意到,當你建立像echo
這樣的泛型函式時,編譯器會強制要求你在函式主體中正確使用任何泛型引數。也就是說,你實際上是把這些引數當作是任何和所有的型別。
讓我們來看看我們前面的echo
函式。
function echo<Type>(arg: Type): Type {
return arg
}
如果我們想在每次呼叫時將引數arg
的長度記錄到控制檯,該怎麼辦?我們可能很想這樣寫:
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
return arg;
}
當我們這樣做時,編譯器會給我們一個錯誤,說我們在使用 arg
的 .length
成員,但我們沒有說arg
有這個成員。記住,我們在前面說過,這些型別的變數可以代表任何和所有的型別,所以使用這個函式的人可以傳入一個 number
型別的數字 ,而這個數字沒有一個 .length
成員。
比方說,我們實際上是想讓這個函式在 Type
的陣列上工作,而不是直接在 Type
上工作。既然我們在 處理陣列,那麼 .length
成員應該是可用的。我們可以像建立其他型別的陣列那樣來描述它。
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}
你可以把 loggingIdentity
的型別理解為 "通用函式 loggingIdentity
接收一個型別引數 Type
和一個引數arg
, arg
是一個 Type 陣列,並返回一個 Type
陣列。" 如果我們傳入一個數字陣列,我們會得到一個數字陣列,因為Type
會繫結到數字。這允許我們使用我們的通用型別變數 Type
作為我們正在處理的型別的一部分,而不是整個型別,給我們更大的靈活性。
我們也可以這樣來寫這個例子:
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length); // 陣列有一個.length,所以不會再出錯了
return arg;
}
你可能已經從其他語言中熟悉了這種型別的風格。在下一節中,我們將介紹如何建立你自己的通用型別,如 Array<Type>
.
7.1.3 泛型介面
在前幾節中,我們建立了在一系列型別上工作的通用身份函式。在這一節中,我們將探討函式本身的型別以及如何建立通用介面。
泛型函式的型別與非泛型函式的型別一樣,型別引數列在前面,與函式宣告類似:
function identity<Type>(arg: Type): Type {
return arg
}
let myIdentity: <Type>(arg: Type) => Type = identity
我們也可以為型別中的通用型別引數使用一個不同的名字,只要型別變數的數量和型別變數的使用方式一致。
function identity<Type>(arg: Type): Type {
return arg
}
let myIdentity: <Input>(arg: Input) => Input = identity
我們也可以把泛型寫成一個物件字面型別的呼叫簽名。
function identity<Type>(arg: Type): Type {
return arg
}
let myIdentity: { <Type>(arg: Type): Type } = identity
這讓我們開始編寫我們的第一個泛型介面。讓我們把前面例子中的物件字面型別移到一個介面中。
interface GenericIdentityFn {
<Type>(arg: Type): Type
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity
在一個類似的例子中,我們可能想把通用引數移到整個介面的引數上。這可以讓我們看到我們的泛型是什麼型別(例如, Dictionary<string>
而不是僅僅 Dictionary
)。這使得型別引數對介面的所有其他成員可見。
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity
請注意,我們的例子已經改變了,變成了稍微不同的東西。我們現在沒有描述一個泛型函式,而是有一個非泛型的函式簽名,它是泛型型別的一部分。當我們使用 GenericIdentityFn
時,我們現在還需要指定相應的型別引數(這裡是:number),有效地鎖定了底層呼叫簽名將使用什麼。瞭解什麼時候把型別引數直接放在呼叫簽名上,什麼時候把它放在介面本身,將有助於描述一個型別的哪些方面是通用的。
除了泛型介面之外,我們還可以建立泛型類。注意,不可能建立泛型列舉和名稱空間。
7.1.4 泛型類
一個泛型類的形狀與泛型介面相似。泛型類在類的名字後面有一個角括號(<>
)中的泛型引數列表.
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType
constructor(zeroValue:NumType, fn: (x: NumType, y: NumType) => NumType ) {
this.zeroValue = zeroValue
this.add = fn
}
}
let myGenericNumber = new GenericNumber<number>(0, function (x, y) {
return x + y;
});
console.log(myGenericNumber.zeroValue) // 0
console.log(myGenericNumber.add(100, 200)) // 300
這是對 GenericNumber
類相當直白的使用,但你可能已經注意到,沒有任何東西限制它只能使用數字型別。我們本可以使用字串或更復雜的物件。
就像介面一樣,把型別引數放在類本身,可以讓我們確保類的所有屬性都與相同的型別一起工作。
正如我們在關於類的章節中提到的,一個類的型別有兩個方面:靜態方面和例項方面。通用類只在其例項側而非靜態側具有通用性,所以在使用類時,靜態成員不能使用類的型別引數。
7.1.5 泛型約束
如果你還記得前面的例子,你有時可能想寫一個通用函式,在一組型別上工作,而你對這組型別會有什麼能力有一定的瞭解。在我們的 loggingIdentity
例子中,我們希望能夠訪問 arg.length
屬性,但是編譯器無法證明每個型別都有一個 .length
屬性,所以它警告我們不能做這個假設:
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
return arg;
}
我們希望限制這個函式與 any
和所有型別一起工作,而不是與 any
和所有同時具有 .length
屬性的型別一起工作。只要這個型別有這個成員,我們就允許它,但它必須至少有這個成員。要做到這一點,我們必須把我們的要求作為一個約束條件列在 Type
可以是什麼。
為了做到這一點,我們將建立一個介面來描述我們的約束。在這裡,我們將建立一個介面,它有一個單一的 .length
屬性,然後我們將使用這個結合 extends
關鍵字來表示我們的約束條件。
interface Lengthwise { //介面聲明瞭一個具有number的物件
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // 現在我們知道它有一個 .length 屬性,所以不再有錯誤了
return arg;
}
因為泛型函式現在被限制了,它將不再對 any
和所有的型別
起作用。
loggingIdentity(3)
相反,我們需要傳入其型別具有所有所需屬性的值。
loggingIdentity({ length: 10, value: 3 });
loggingIdentity(["sdas",'sdasd'])
7.1.6 在泛型約束中使用型別引數
你可以宣告一個受另一個型別引數約束的型別引數。例如,在這裡我們想從一個給定名稱的物件中獲取一個屬性。我們想確保我們不會意外地獲取一個不存在於 obj
上的屬性,所以我們要在這兩種型別之間放置一個約束條件。
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
7.1.7 在泛型中使用類型別
在TS中使用泛型建立工廠時,有必要通過其建構函式來引用類的型別。比如說:
function create<Type>(c: { new (): Type }): Type {
return new c();
}
一個更高階的例子,使用原型屬性來推斷和約束類型別的建構函式和例項方之間的關係。
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
const lionNametag:string = createInstance(Lion).keeper.nametag;
const BeeMask:boolean = createInstance(Bee).keeper.hasMask;
console.log(lionNametag, BeeMask) // Mikle true
7.2 keyOf
型別操作符
keyof
運算子接收一個物件型別,並產生其鍵的字串或數字字面聯合。下面的型別P
與 "x"|"y "
是同一型別。
type Point = { x: number; y: number };
type P = keyof Point;
const p1:P = 'x'
const p2:P = 'y'
如果該型別有一個字串或數字索引簽名, keyof
將返回這些型別。
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish; // number
const a:A = 0
type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // string|number
const m:M = 'a'
const m2:M = 10
注意,在這個例子中, M
是 string|number
——這是因為JavaScript物件的鍵總是被強制為字串,所以 obj[0]
總是與obj["0"]
相同.
keyof
型別在與對映型別結合時變得特別有用,我們將在後面進一步瞭解。
7.3 typeof
型別操作符
JavaScript已經有一個 typeof
操作符,你可以在表示式上下文中使用。
// 輸出 "string"
console.log(typeof "Hello world");
TypeScript添加了一個 typeof
操作符,你可以在型別上下文中使用它來引用一個變數或屬性的型別
let s = "hello";
let n: typeof s;
n = 'world'
n= 100
這對基本型別來說不是很有用,但結合其他型別操作符,你可以使用typeof
來方便地表達許多模式。舉一個例子,讓我們先看看預定義的型別 ReturnType<T>
。它接收一個函式型別併產生其返回型別:
type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>;
如果我們試圖在一個函式名上使用 ReturnType
,我們會看到一個指示性的錯誤。
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<f>
請記住,值和型別並不是一回事。為了指代值f
的型別,我們使用 typeof
。
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>
TypeScript 故意限制了你可以使用 typeof
的表示式種類。
具體來說,只有在識別符號(即變數名)或其屬性上使用typeof
是合法的。這有助於避免混亂的陷阱,即編寫你認為是在執行的程式碼,但其實不是。
// 我們認為使用 = ReturnType<typeof msgbox>
let shouldContinue: typeof msgbox("Are you sure you want to continue?");
7.4 索引訪問型別
我們可以使用一個索引訪問型別來查詢另一個型別上的特定屬性:
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];
let age1: Age = 100
let age2: Age = "1100"
索引型別本身就是一個型別,所以我們可以完全使用 unions
、 keyof
或者其他型別。
interface Person {
name: string
age: number
alive: boolean
}
// type I1 = string | number
type I1 = Person["age" | "name"];
const i11:I1 = 100
const i12:I1 = ''
// type I2 = string | number | boolean
type I2 = Person[keyof Person];
const i21:I2 = ''
const i22:I2 = 100
const i23:I2 = false
// type I3 = Person[AliveOrName];
type AliveOrName = "alive" | "name";
const aon1:AliveOrName = 'alive'
const aon2:AliveOrName = 'name'
如果你試圖索引一個不存在的屬性,你甚至會看到一個錯誤:
type I1 = Person["alve"]
另一個使用任意型別進行索引的例子是使用 number
來獲取一個數組元素的型別。我們可以把它和typeof
結合起來,方便地獲取一個數組字面的元素型別。
const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];
/* type Person = {
name: string;
age: number;
} */
type Person = typeof MyArray[number];
const p:Person = {
name: 'xiaoqian',
age: 11
}
// type Age = number
type Age = typeof MyArray[number]["age"];
const age:Age = 11
// 或者
// type Age2 = number
type Age2 = Person["age"];
const age2:Age2 = 11
你只能在索引時使用型別,這意味著你不能使用 const
來做一個變數引用:
const key = "age";
type Age = Person[key];
然而,你可以使用類型別名來實現類似的重構風格:
type key = "age";
type Age = Person[key];
7.5 條件型別
在大多數有用的程式的核心,我們必須根據輸入來做決定。JavaScript
程式也不例外,但鑑於數值可以很容易地被內省,這些決定也是基於輸入的型別。條件型別有助於描述輸入和輸出的型別之間的關係。
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
// type Example1 = number
type Example1 = Dog extends Animal ? number : string;
// type Example2 = string
type Example2 = RegExp extends Animal ? number : string;
條件型別的形式看起來有點像JavaScript中的條件表示式( condition? trueExpression : falseExpression
)
SomeType extends OtherType ? TrueType : FalseType;
當 extends 左邊的型別可以賦值給右邊的型別時,那麼你將得到第一個分支中的型別("真 "分支); 否則你將得到後一個分支中的型別("假 "分支)。
子類可以賦值給父類!!!
從上面的例子來看,條件型別可能並不立即顯得有用——我們可以告訴自己是否 Dog extends Animal
,並選擇 number
或 string
!
但條件型別的威力來自於它所帶來的好處。條件型別的力量來自於將它們與泛型一起使用。
例如,讓我們來看看下面這個 createLabel
函式:
interface IdLabel {
id: number /* 一些欄位 */;
}
interface NameLabel {
name: string /* 另一些欄位 */;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}
createLabel
的這些過載描述了一個單一的JavaScript函式,該函式根據其輸入的型別做出選擇。注意 一些事情:
- 如果一個庫必須在其API中反覆做出同樣的選擇,這就會變得很麻煩。
- 我們必須建立三個過載:一個用於確定型別的情況(一個用於
string
,一個用於number
),一個用於最一般的情況(取一個string | number
)。對於createLabel
所能處理的每一種新型別,過載的數量都會呈指數級增長。
相反,我們可以在一個條件型別中對該邏輯進行編碼:
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
然後我們可以使用該條件型別,將我們的過載簡化為一個沒有過載的單一函式。
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
type NameOrId<T extends number | string> = T extends number? IdLabel: NameLabel;
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
// let a: NameLabel
let a = createLabel("typescript");
// let b: IdLabel
let b = createLabel(2.8);
// let c: NameLabel | IdLabel
let c = createLabel(Math.random() ? "hello" : 42);
7.5.1 條件型別約束
通常,條件型別中的檢查會給我們提供一些新的資訊。就像用型別守衛縮小範圍可以給我們一個更具體的型別一樣,條件型別的真正分支將通過我們檢查的型別進一步約束泛型。
例如, 讓我們來看下面的例子:
type Message<T> = T["message"]
在這個例子中,TypeScript出錯是因為 T
不知道有一個叫做 message
的屬性。我們可以對 T
進行約束,TypeScript就不會再抱怨。
type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>;
然而,如果我們想讓 MessageOf
接受任何型別,並在訊息屬性不可用的情況下,預設為 never
型別 呢?我們可以通過將約束條件移出,並引入一個條件型別來做到這一點。
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
// type EmailMessageContents = string
type EmailMessageContents = MessageOf<Email>;
const emc:EmailMessageContents = 'balabala...'
// type DogMessageContents = never
type DogMessageContents = MessageOf<Dog>;
const dmc:DogMessageContents = 'error' as never
在真正的分支中,TypeScript知道 T
會有一個訊息屬性。
作為另一個例子,我們也可以寫一個叫做 Flatten
的型別,將陣列型別平鋪到它們的元素型別上,但在其他方面則不做處理.
type Flatten<T> = T extends any[] ? T[number] : T;
// 提取出元素型別
// type Str = string
type Str = Flatten<string[]>
// 單獨一個型別。
// type Num = number
type Num = Flatten<number>
當 Flatten
被賦予一個數組型別時,它使用一個帶有數字的索引訪問來獲取 string[]
的元素型別。 否則,它只是返回它被賦予的型別。
7.5.2 在條件型別內進行推理
我們只是發現自己使用條件型別來應用約束條件,然後提取出型別。這最終成為一種常見的操作,而條件型別使它變得更容易。
條件型別為我們提供了一種方法來推斷我們在真實分支中使用 infer
關鍵字進行對比的型別。例如, 我們可以在 Flatten
中推斷出元素型別,而不是用索引訪問型別 "手動 "提取出來。
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type
在這裡,我們使用 infer
關鍵字來宣告性地引入一個名為 Item
的新的通用型別變數,而不是指定如何在真實分支中檢索 Type
的元素型別。這使我們不必考慮如何挖掘和探測我們感興趣的型別的結構。
我們可以使用 infer
關鍵字編寫一些有用的輔助類型別名。例如,對於簡單的情況,我們可以從函式型別中提取出返回型別。
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return? Return : never;
// type Num = number
type Num = GetReturnType<() => number>;
// type Str = string
type Str = GetReturnType<(x: string) => string>;
// type Bools = boolean[]
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
// 給泛型傳入 string 型別,條件型別會返回 never
type Never = GetReturnType<string>
const nev:Never = 'error' as never
當從一個具有多個呼叫簽名的型別(如過載函式的型別)進行推斷時,從最後一個簽名進行推斷(據推測,這是最容許的萬能情況)。不可能根據引數型別的列表來執行過載解析。
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
// type T1 = string | number
type T1 = ReturnType<typeof stringOrNum>;
7.5.3 分散式條件型別
當條件型別作用於一個通用型別時,當給定一個聯合型別時,它們就變成了分散式的。例如,以下面的例子為例:
type ToArray<Type> = Type extends any? Type[] : never;
如果我們將一個聯合型別插入ToArray
,那麼條件型別將被應用於該聯合的每個成員。
type ToArray<Type> = Type extends any ? Type[] : never;
// type StrArrOrNumArr = string[] | number[]
type StrArrOrNumArr = ToArray<string | number>;
這裡發生的情況是,StrArrOrNumArr
分佈在:
string | number;
並對聯合的每個成員型別進行對映,以達到有效的目的:
ToArray<string> | ToArray<number>;
這給我們留下了:
string[] | number[];`
通常情況下,分佈性是需要的行為。為了避免這種行為,你可以用方括號包圍 extends
關鍵字的每一邊。
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'StrArrOrNumArr'不再是一個聯合型別
// type StrArrOrNumArr = (string | number)[]
type StrArrOrNumArr = ToArrayNonDist<string | number>;
7.6 對映型別
當你不想重複定義型別,一個型別可以以另一個型別為基礎建立新型別。
對映型別建立在索引簽名的語法上,索引簽名用於宣告沒有被提前宣告的屬性型別。
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse
}
const confroms: OnlyBoolsAndHorses = {
del: true,
rodney: false
}
對映型別是一種通用型別,它使用 Property in keyof Type
的聯合(經常通過 keyof
建立)迭代鍵來建立一個型別。
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
在這個例子中, OptionsFlags
將從 Type
型別中獲取所有屬性,並將它們的值改為布林值。
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
/*
type FeatureOptions = {
darkMode: boolean;
newUserProfile: boolean;
}
*/
type FeatureOptions = OptionsFlags<FeatureFlags>;
7.6.1 對映修改器
在對映過程中,有兩個額外的修飾符可以應用: readonly
和 ?
,它們分別影響可變性和可選性。
你可以通過用 -
或 +
作為字首來刪除或新增這些修飾語。如果你不加字首,那麼就假定是 +
。
type CreateMutable<Type> = {
// 從一個型別的屬性中刪除 "readonly"屬性
-readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
readonly id: string;
readonly name: string;
};
/*
type UnlockedAccount = {
id: string;
name: string;
}
*/
type UnlockedAccount = CreateMutable<LockedAccount>
// 從一個型別的屬性中刪除 "可選" 屬性
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
/*
type User = {
id: string;
name: string;
age: number;
}
*/
type User = Concrete<MaybeUser>;
7.6.2 通過 as
做key
重對映
在TypeScript 4.1及以後的版本中,你可以通過對映型別中的as
子句重新對映對映型別中的鍵。
type MappedTypeWithNewProperties<Type> = {
[Properties in keyof Type as NewKeyType]: Type[Properties]
}
你可以利用模板字面型別
等功能,從先前的屬性名稱中建立新的屬性名稱。
Capitalize<string & Property>
來是string首字母大寫
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () =>Type[Property]
};
interface Person {
name: string;
age: number;
location: string;
}
/*
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
}
*/
type LazyPerson = Getters<Person>;
你可以通過條件型別產生 never
濾掉的鍵。
Exclude<Property, "kind">
過濾掉key為"kind"的鍵
// 刪除 "kind"屬性
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
/*
type KindlessCircle = {
radius: number;
}
*/
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
你可以對映任意的聯合體,不僅僅是 string | number | symbol
的聯合體,還有任何型別的聯合體.
type EventConfig<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
}
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
/*
type Config = {
square: (event: SquareEvent) => void;
circle: (event: CircleEvent) => void;
}
*/
type Config = EventConfig<SquareEvent | CircleEvent>
7.6.3 進一步探索
對映型別與本型別操作部分的其他功能配合得很好,例如,這裡有一個使用條件型別的對映型別 ,它根據一個物件的屬性 pii
是否被設定為字面意義上的 true
,返回 true
或 false
.
type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
/*
type ObjectsNeedingGDPRDeletion = {
id: false;
name: true;
}
*/
type DBFields = {
id: { format: "incrementing" };
name: { type: string; pii: true };
};
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>
TypeScript學習第八章: 類
TS提供了對ES2015(ES6)中引入的class
關鍵詞的完全支援.
與其他的JS語言一樣,TS增加了型別註釋和其他語法, 允許你表達類和其他型別之間的關係.
8.1 類成員
這裡有一個最基本的類------一個空的類
class Point {}
這個類還不是很有用, 所以我們開始新增一些成員.
8.1.1 類屬性
在一個類上宣告欄位, 建立一個公共的可寫屬性: 對映型別是一種泛型型別,它使用PropertyKey
(通常通過key of
建立)的聯合來迭代鍵來建立型別:
class Point {
x: number
y: number
}
const pt = new Point()
pt.x = 0
pt.y = 0
與其他位置一樣,型別註解是可選的,但如果不指定,將是一個隱含的 any
型別。
欄位也可以有初始化器;這些初始化器將在類被例項化時自動執行。
class Point {
x = 0
y = 0
}
const pt = new Point()
// Prints 0, 0
console.log(`${pt.x}, ${pt.y}`)
就像 const
、 let
和 var
一樣,一個類屬性的初始化器將被用來推斷其型別。
const pt = new Point();
pt.x = "0";
--strictPropertyInitialization
strictPropertyInitialization
設定控制是否需要在建構函式中初始化類欄位。
class GoodGreeter {
name: string;
constructor() {
this.name = "hello";
}
}
請注意,該欄位需要在建構函式本身中初始化。TypeScript不會分析你從建構函式中呼叫的方法來檢測初始化,因為派生類可能會覆蓋這些方法而無法初始化成員。
如果你打算通過建構函式以外的方式來確定初始化一個欄位(例如,也許一個外部庫為你填充了你的類的一部分),你可以使用確定的賦值斷言操作符 !
。
class OKGreeter {
// 沒有初始化, 但沒報錯
name!: string
}
8.1.2 readonly
欄位的字首可以是readonly
修飾符。這可以防止在建構函式之外對該欄位進行賦值。
class Greeter {
readonly name: string = "world";
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
err() {
this.name = "not ok";
}
}
const g = new Greeter();
g.name = "also not ok";
8.1.3 構造器
類建構函式與函式非常相似。你可以新增帶有型別註釋的引數、預設值和過載:
class Point {
x: number
y: number
// 帶預設值的正常簽名
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
class Point {
x: number;
y: string;
// 過載
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
if(y !== undefinded) {
this.x = xs
this.y = y
} else {
this.y = xs
}
}
}
類的建構函式簽名和函式簽名之間只有一些區別:
- 建構函式不能有型別引數--這屬於外層類的宣告,我們將在後面學習。
- 建構函式不能有返回型別註釋——類的例項型別總是被返回的.
Super 呼叫
就像在JavaScript中一樣,如果你有一個基類,在使用任何 this.
成員之前,你需要在構造器主體中呼叫 super();
.
class Base {
k = 4;
}
class Derived extends Base {
constructor() {
// 在ES5中列印一個錯誤的值;在ES6中丟擲異常。
console.log(this.k);
super();
}
}
在JavaScript中,忘記呼叫 super
是一個很容易犯的錯誤,但TypeScript會在必要時告訴你。
8.1.4 方法
一個類上的函式屬性被稱為方法。方法可以使用與函式和建構函式相同的所有型別註釋。
class Point {
x = 10;
y = 10;
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
除了標準的型別註解,TypeScript並沒有為方法新增其他新的東西。
請注意,在一個方法體中,仍然必須通過 this 訪問欄位和其他方法。方法體中的非限定名稱將總是指代包圍範圍內的東西。
let x: number = 0;
class C {
x: string = "hello";
m() {
// 這是在試圖修改第1行的'x',而不是類屬性。
x = "world";
}
}
8.1.5 Getters/ Setters
類也可以有訪問器:
class C {
_length = 0
get length() {
return this._length
}
set length(value) {
this._length = value
}
}
請注意,一個沒有額外邏輯的欄位支援的 get/set 對在JavaScript中很少有用。如果你不需要在 get/set 操作中新增額外的邏輯,暴露公共欄位也是可以的.
TypeScript對訪問器有一些特殊的推理規則:
-
如果存在
get
,但沒有set
,則該屬性自動是隻讀的. -
如果沒有指定
setter
引數的型別,它將從getter
的返回型別中推斷出來. -
訪問器和設定器必須有相同的成員可見性.
從TS4.3開始, 可以有不同型別的訪問器用於獲取和設定.
class Thing {
_size = 0;
get size(): number {
return this._size;
}
set size(value: string | number | boolean) {
let num = Number(value);
// 不允許NaN、Infinity等
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
this._size = num;
}
}
8.1.6 索引簽名
類可以宣告索引簽名;這些簽名的作用與其他物件型別的索引簽名相同。
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
check(s: string) {
return this[s] as boolean;
}
}
因為索引簽名型別需要同時捕獲方法的型別,所以要有用地使用這些型別並不容易。一般來說,最好將索引資料儲存在另一個地方,而不是在類例項本身。
8.2 類繼承
像其他具有面向物件特性的語言一樣,JavaScript中的類可以繼承自基類。
8.2.1 implements
子句
你可以使用一個 implements
子句來檢查一個類,是否滿足了一個特定的介面。如果一個類不能正確地實現它,就會發出一個錯誤。
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable {
pong() {
console.log("pong!");
}
}
類也可以實現多個介面, 例如class C implements A, B {}
注意事項
重要的是要明白, implements
子句只是檢查類是否可以被當作介面型別來對待. 它根本不會改變類的型別或方法. 一個常見的錯誤來源於是認為implements
子句會改變類的型別, 實際上它不會.
interface Checkable {
check(name:string): boolean
}
class NameChecker implements Checkable {
check(s) {
// any: 注意這裡沒有錯誤
return s.toLowercse() === 'ok'
}
}
在這個例子中,我們也許期望 s 的型別會受到 check
的 name: string
引數的影響。事實並非如此--實現子句並沒有改變類主體的檢查方式或其型別的推斷。
同樣地,實現一個帶有可選屬性的介面並不能建立該屬性。
interface A {
x: number;
y?: number;
}
class C implements A {
x = 0;
}
const c = new C();
c.y = 10;
8.2.2 extends
子句
類可以從基類中擴展出來。派生類擁有其基類的所有屬性和方法,也可以定義額外的成員。
class Animal {
move() {
console.log("Moving along!");
}
}
class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log("woof!");
}
}
}
const d = new Dog();
// 基類的類方法
d.move();
// 派生的類方法
d.woof(3);
8.2.3 重寫方法(遵守基類契約)
派生類也可以覆蓋基類的一個欄位或屬性. 你可以使用super.
語法來訪問基類方法. 注意,因為JavaScript類是一個簡單的查詢物件,沒有 "超級欄位 "的概念.
TypeScript強制要求派生類總是其基類的一個子型別.
例如,這裡有一個合法的方法來覆蓋一個方法.
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
const d = new Derived();
d.greet();
d.greet("reader");
派生類遵循其基類契約是很重要的。請記住,通過基類引用來引用派生類例項是非常常見的(而且總是合法的!)。
// 通過基類引用對派生例項進行取別名
const b: Base = d;
// 沒問題
b.greet();
如果Derived
沒有遵守Base
的約定怎麼辦?
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
// 使這個引數成為必需的
greet(name: string) {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
如果我們不顧錯誤編譯這段程式碼,這個樣本就會崩潰:
const b: Base = new Derived();
// 崩潰,因為 "名稱 "將是 undefined。
b.greet();
8.2.4 初始化順序
在某些情況下,JavaScript類的初始化順序可能會令人驚訝。讓我們考慮一下這段程式碼:
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
class Derived extends Base {
name = "derived";
}
// 列印 "base", 而不是 "derived"
const d = new Derived();
這裡發生了什麼?
按照JavaScript的定義,類初始化的順序是:
- 基類的欄位被初始化
- 基類建構函式執行
- 派生類的欄位被初始化
- 派生類建構函式執行
這意味著基類建構函式在自己的建構函式中看到了自己的name
值,因為派生類的欄位初始化還沒有執行.
8.2.5 繼承內建型別
注意: 如果你不打算繼承Array、Error、Map等內建型別,或者你的編譯目標明確設定為 ES6/ES2015或以上,你可以跳過本節.
在ES2015中,返回物件的建構函式隱含地替代了 super(...)
的任何呼叫者的 this
的值。生成的建構函式程式碼有必要捕獲 super(...)
的任何潛在返回值並將其替換為 this
。
因此,子類化 Error
、 Array
等可能不再像預期那樣工作。這是由於 Error
、 Array
等的建構函式使用ECMAScript 6的 new.target
來調整原型鏈;然而,在ECMAScript 5中呼叫建構函式時,沒有辦法確保 new.target
的值。其他的下級編譯器一般預設有同樣的限制。
對於一個像下面這樣的子類:
class MsgError extends Error {
constructor(m: string) {
super(m);
}
sayHello() {
return "hello " + this.message;
}
}
你可能會發現:
- 方法在構造這些子類所返回的物件上可能是未定義的,所以呼叫
sayHello
會導致錯誤。 -
instanceof
將在子類的例項和它們的例項之間被打破,所以(new MsgError()) instanceof MsgError
將返回false
。
作為建議, 你可以在任何super(...)
呼叫後立即手動調整原型.
class MsgError extends Error {
constructor(m: string) {
super(m);
// 明確地設定原型。
Object.setPrototypeOf(this, MsgError.prototype);
}
sayHello() {
return "hello " + this.message;
}
}
然而, MsgError
的任何子類也必須手動設定原型。對於不支援 Object.setPrototypeOf
的執行時, 你可以使用__proto__
來代替。
不幸的是,這些變通方法在IE10 和更早的版本上不起作用。我們可以手動將原型中的方法複製到例項本身(例如 MsgError.prototype
到 this
),但是原型鏈本身不能被修復。
8.3 成員的可見性
你可以使用TypeScript來控制某些方法或屬性對類外的程式碼是否可見.
8.3.1 public
類成員的預設可見性是公共( public
)的。一個公共( public
)成員可以在任何地方被訪問.
class Greeter {
public greet() {
console.log("hi!");
}
}
const g = new Greeter();
g.greet();
因為 public
已經是預設的可見性修飾符,所以你永遠不需要在類成員上寫它,但為了風格/可讀性的原因,可能會選擇這樣做。
8.3.2 protected
受保護的( protected
)成員只對它們所宣告的類的子類可見.
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
class SpecialGreeter extends Greeter {
public howdy() {
// 在此可以訪問受保護的成員
console.log("Howdy, " + this.getName());
}
}
const g = new SpecialGreeter();
g.greet(); // 沒有問題
g.getName(); // 無權訪問
- 受保護成員的暴露
派生類需要遵循它們的基類契約,但可以選擇公開具有更多能力的基類的子型別。這包括將受保護的成員變成公開。
class Base {
protected m = 10;
}
class Derived extends Base {
// 沒有修飾符,所以預設為'公共'('public')
m = 15;
}
const d = new Derived();
console.log(d.m); // OK
8.2.3 private
private
和protected
一樣, 但不允許從子類中訪問該成員.
class Base {
private x = 0;
}
const b = new Base();
// 不能從類外訪問
console.log(b.x);
class Base {
private x = 0;
}
const b = new Base();
class Derived extends Base {
showX() {
// 不能在子類中訪問: 屬性"x"為私有屬性, 只能在類
console.log(this.x);
}
}
因為私有( private
)成員對派生類是不可見的,所以派生類不能增加其可見性.
- 跨例項的私有訪問
不同的OOP語言對同一個類的不同例項,是否可以訪問對方的私有成員,有不同的處理方法。雖然像 Java、C#、C++、Swift和PHP等語言允許這樣做,但Ruby不允許。
TypeScript確實允許跨例項的私有訪問:
class A {
private x = 10;
public sameAs(other: A) {
// 可以訪問
return other.x === this.x;
}
}
- 注意事項
像TypeScript型別系統的其他方面一樣, private
和 protected
只在型別檢查中被強制執行。
這意味著JavaScript的執行時解構,如in
或簡單的屬性查詢,仍然可以訪問一個私有或保護的成員。
class MySafe {
private secretKey = 12345;
}
// 在JS環境中...
const s = new MySafe();
// 將列印 12345
console.log(s.secretKey);
private
也允許在型別檢查時使用括號符號進行訪問。這使得私有宣告的欄位可能更容易被單元測試之類的東西所訪問,缺點是這些欄位是軟性私有的,不能嚴格執行私有特性。
class MySafe {
private secretKey = 12345;
}
const s = new MySafe();
// 在型別檢查期間不允許
console.log(s.secretKey);
// 正確
console.log(s["secretKey"]);
與TypeScript的 private
不同,JavaScript的 private
欄位(#)在編譯後仍然是 private
的,並且不提供前面提到的像括號符號訪問那樣的轉義視窗,使其成為硬 private
.
class Dog {
#barkAmount = 0;
personality = "happy";
constructor() {
// 0
console.log(this.#barkAmount)
}
}
const dog = new Dog()
// undefined
console.log(dog.barkAmount)
當編譯到ES2021或更少時,TypeScript將使用WeakMaps
來代替 #
。
"use strict";
var _Dog_barkAmount;
class Dog {
constructor() {
_Dog_barkAmount.set(this, 0);
this.personality = "happy";
}
}
_Dog_barkAmount = new WeakMap();
如果你需要保護你的類中的值免受惡意行為的影響,你應該使用提供硬執行時隱私的機制,如閉包、 WeakMaps
或私有欄位。請注意,這些在執行時增加的隱私檢查可能會影響效能。
8.4 靜態成員
類可以有靜態成員。這些成員並不與類的特定例項相關聯。它們可以通過類的建構函式物件本身來訪問。
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}
console.log(MyClass.x);
MyClass.printX();
靜態成員也可以使用相同的 public
、 protected
和 private
可見性修飾符
class MyClass {
private static x = 0;
}
console.log(MyClass.x);
靜態成員也會被繼承
class Base {
static getGreeting() {
return "Hello world";
}
}
class Derived extends Base {
myGreeting = Derived.getGreeting();
}
8.4.1 特殊靜態名稱
一般來說,從函式原型覆蓋屬性是不安全的/不可能的。因為類本身就是可以用 new
呼叫的函式,所以某些靜態名稱不能使用。像 name
、 length
和call
這樣的函式屬性,定義為靜態成員是無效的。
class S {
static name = "S!"
}
8.4.2 為什麼沒有靜態類?
TypeScript(和JavaScript)沒有像C#和Java那樣有一個叫做靜態類的結構。
這些結構體的存在,只是因為這些語言強制所有的資料和函式都在一個類裡面;因為這個限制在TypeScript中不存在,所以不需要它們。一個只有一個例項的類,在JavaScript/TypeScript中通常只是表示為一個普通的物件。
例如,我們不需要TypeScript中的 "靜態類 "語法,因為一個普通的物件(甚至是頂級函式)也可以完成這個工作。
// 不需要 "static" class
class MyStaticClass {
static doSomething() {}
}
// 首選 (備選 1)
function doSomething() {}
// 首選 (備選 2)
const MyHelperObject = {
dosomething() {},
};
8.5 類裡的static
區塊
靜態塊允許你寫一串有自己作用域的語句,可以訪問包含類中的私有欄位。這意味著我們可以用寫語句的所有能力來寫初始化程式碼,不洩露變數,並能完全訪問我們類的內部結構。
class Foo {
static #count = 0;
get count() {
return Foo.#count;
}
static {
try {
const lastInstances = {
length: 100
};
Foo.#count += lastInstances.length;
}
catch {}
}
}
8.6 泛型類
類,和介面一樣,可以是泛型的。當一個泛型類用new
例項化時,其型別引數的推斷方式與函式呼叫的方式相同。
class Box<Type> {
contents: Type
constructor(value: Type) {
this.contents = value
}
}
// const b: Box<string>
const b = new Box("hello")
類可以像介面一樣使用通用約束和預設值.
- 靜態成員中的型別引數
這段程式碼事不合法的, 可能不太明顯, 為什麼呢?
class Box<Type> {
// 靜態成員不能引用類的型別引數。
static defaultValue: Type;
}
// Box<string>.defaultValue = 'hello'
// console.log(Box<number>.defaultValue)
請記住,型別總是被完全擦除的! 在執行時,只有一個Box.defaultValue
屬性。這意味著設定Box.defaultValue
(如果有可能的話)也會改變Box.defaultValue
,這可不是什麼好事。
一個泛型類的靜態成員永遠不能引用該類的型別引數.
8.7 類執行時的this
重要的是要記住,TS並沒有改變JS的執行時行為,而JavaScript的執行時行為偶爾很奇特。
比如,JavaScript對這一點的處理確實是不尋常的:
class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
// 輸出 "obj", 而不是 "MyClass"
console.log(obj.getName())
長話短說,預設情況下,函式內this的值取決於函式的呼叫方式。在這個例子中,因為函式是通過obj引用呼叫的,所以它的this值是obj而不是類例項。
這很少是你希望發生的事情! TypeScript提供了一些方法來減輕或防止這種錯誤.
1. 箭頭函式
如果你有一個經常會被呼叫的函式,失去了它的 this
上下文,那麼使用一個箭頭函式而不是方法定義是有意義的。
class MyClass {
name = "MyClass";
getName = () => {
return this.name;
};
}
const c = new MyClass();
const g = c.getName;
// 輸出 "MyClass"
console.log(g());
這有一些權衡:
- this 值保證在執行時是正確的,即使是沒有經過TypeScript檢查的程式碼也是如此。
- 這將使用更多的記憶體,因為每個類例項將有它自己的副本,每個函式都是這樣定義的。
- 你不能在派生類中使用
super.getName
,因為在原型鏈中沒有入口可以獲取基類方法。
2. this
引數
在方法或函式定義中,一個名為 this
的初始引數在TypeScript中具有特殊的意義。這些引數在編譯過程中會被刪除。
// 帶有 "this" 引數的 TypeScript 輸入
function fn(this: SomeType, x: number) {
/* ... */
}
// 編譯後的JavaScript結果
function fn(x) {
/* ... */
}
TypeScript檢查呼叫帶有 this
引數的函式,是否在正確的上下文中進行。我們可以不使用箭頭函式,而是在方法定義中新增一個 this
引數,以靜態地確保方法被正確呼叫.
class MyClass {
name = "MyClass";
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
// 正確
c.getName();
// 錯誤
const g = c.getName;
console.log(g());
這種方法做出了與箭頭函式方法相反的取捨:
- JavaScript呼叫者仍然可能在不知不覺中錯誤地使用類方法
- 每個類定義只有一個函式被分配,而不是每個類例項一個函式
- 基類方法定義仍然可以通過 super 呼叫。
8.8 this
型別
在類中,一個叫做 this
的特殊型別動態地指向當前類的型別。讓我們來看看這有什麼用:
class Box {
contents: string = "";
// (method) Box.set(value: string): this
set(value: string) {
this.contents = value;
return this;
}
}
在這裡,TypeScript推斷出 set
的返回型別是 this
,而不是 Box
。現在讓我們做一個Box
的子類:
class ClearableBox extends Box {
clear() {
this.contents = "";
}
}
const a = new ClearableBox();
// const b: ClearableBox
const b = a.set("hello");
console.log(b)
你也可以在引數型別註釋中使用 this
:
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
const box = new Box()
console.log(box.sameAs(box))
這與其他寫法不同:Box,如果你有一個派生類,它的sameAs
方法現在只接受該同一派生類的其他例項。
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
class DerivedBox extends Box {
otherContent: string = "?";
}
const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base); // 報錯 型別 "Box" 中缺少屬性 "otherContent",但型別 "DerivedBox" 中需要該屬性。
8.9 基於型別守衛的this
(???不太會)
你可以在類和介面的方法的返回位置使用 this is Type
。當與型別縮小混合時(例如if
語句),目標物件的型別將被縮小到指定的Type。
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
isNetworked(): this is Networked & this {
return this.networked;
}
constructor(public path: string, private networked: boolean) {
}
}
class FileRep extends FileSystemObject {
constructor(path: string, public content: string) {
super(path, false);
}
}
class Directory extends FileSystemObject {
children: FileSystemObject[];
}
interface Networked {
host: string;
}
const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
if (fso.isFile()) {
// const fso: FileRep
fso.content;
} else if (fso.isDirectory()) {
// const fso: Directory
fso.children;
} else if (fso.isNetworked()) {
// const fso: Networked & FileSystemObject
fso.host;
}
基於 this
的型別保護的一個常見用例,是允許對一個特定欄位進行懶惰驗證。例如,這種情況下,當hasValue 被驗證為真時,就會從框內持有的值中刪除一個未定義值。
class Box <T> {
value?: T;
hasValue(): this is { value: T} {
return this.value !== undefined;
}
}
const box = new Box();
box.value = "Gameboy";
// (property) Box<unknown>.value?: unknownbox.value;
if (box.hasValue()) {
// (property) value: unknown
box.value;
}
8.10 引數屬性,建構函式引數轉類屬性
TypeScript提供了特殊的語法,可以將建構函式引數變成具有相同名稱和值的類屬性。這些被稱為引數屬性,通過在建構函式引數前加上可見性修飾符 public
、 private
、 protected
或 readonly
中的一個來建立。由此產生的欄位會得到這些修飾符.
class Params {
constructor(public readonly x: number, protected y: number, private z: number)
{
// No body necessary
}
}
const a = new Params(1, 2, 3);
// (property) Params.x: number
console.log(a.x);
console.log(a.z);
8.11 類表示式
類表示式與類宣告非常相似。唯一真正的區別是,類表示式不需要一個名字,儘管我們可以通過它們最終繫結的任何識別符號來引用它們。
const someClass = class<Type> {
content: Type;
constructor(value: Type) {
this.content = value;
}
};
// const m: someClass<string>
const m = new someClass("Hello, world");
8.12 抽象類和成員
TypeScript中的類、方法和欄位可以是抽象的。
一個抽象的方法或抽象的欄位是一個沒有提供實現的方法或欄位。這些成員必須存在於一個抽象類中, 不能直接例項化。
抽象類的作用是作為子類的基類,實現所有的抽象成員。當一個類沒有任何抽象成員時,我們就說它是具體的。
讓我們看一個例子:
abstract class Base {
abstract getName(): string;
printName() {
console.log("Hello, " + this.getName());
}
}
const b = new Base();
我們不能用 new
來例項化 Base
,因為它是抽象的。相反,我們需要建立一個派生類並實現抽象成員.
class Derived extends Base {
getName() {
return "world";
}
}
const d = new Derived();
d.printName();
- 抽象構造簽名
有時你想接受一些類的建構函式,產生一個從某些抽象類派生出來的類的例項。
例如,你可能想寫這樣的程式碼:
function greet(ctor: typeof Base) {
const instance = new ctor();
instance.printName();
}
TypeScript正確地告訴你,你正試圖例項化一個抽象類。畢竟,鑑於greet的定義,寫這段程式碼是完全合 法的,它最終會構造一個抽象類.
// 槽糕
greet(Base);
相反,你想寫一個函式,接受具有結構化簽名的東西:
function greet(ctor: new() => Base) {
const instance = new ctor();
instance.printName();
}
greet(Derived);
greet(Base);
現在TypeScript正確地告訴你哪些類的建構函式可以被呼叫:Derived
可以,因為它是具體的,但Base
不能。
8.13 類之間的關係
在大多數情況下,TypeScript中的類在結構上與其他型別相同,是可以比較的。
例如,這兩個類可以互相替代使用,因為它們是相同的:
class Point1 {
x = 0;
y = 0;
}
class Point2 {
x = 0;
y = 0;
}
// 正確
const p: Point1 = new Point2()
同樣地,即使沒有明確的繼承,類之間的子型別關係也是存在的:
class Person {
name: string;
age: number;
}
class Employee {
name: string;
age: number;
salary: number;
}
// 正確
const p: Person = new Employee();
這聽起來很簡單,但有幾種情況似乎比其他情況更奇怪。
空的類沒有成員。在一個結構化型別系統中,一個沒有成員的型別通常是其他任何東西的超型別。所以如果你寫了一個空類(不要!),任何東西都可以用來代替它。
class Empty {
}
function fn(x: Empty) {
// 不能用'x'做任何事
}
// 以下呼叫均可
!fn(window);
fn({});
fn(fn);
TypeScript學習第九章:模組
JavaScript有很長的歷史,有不同的方式來處理模組化的程式碼。TypeScript從2012年開始出現,已經實現了對許多這些格式的支援,但隨著時間的推移,社群和JavaScript規範已經趨向於一種名為ES模組 (或ES6模組)的格式。你可能知道它是 import/export 語法。
ES Modules在2015年被加入到JavaScript規範中,到2020年,在大多數網路瀏覽器和JavaScript執行時中都有廣泛的支援。
為了突出重點,本手冊將涵蓋ES Modules及其流行的前驅CommonJS module.exports =
語法。
9.1 如何定義JavaScript模組
在TypeScript中,就像在ECMAScript 2015中一樣,任何包含頂級import
或export
的檔案都被認為是 一個模組。
相反,一個沒有任何頂級匯入或匯出宣告的檔案被視為一個指令碼,其內容可在全域性範圍內使用(因此也可用於模組)。
模組在自己的範圍內執行,而不是在全域性範圍內。這意味著在模組中宣告的變數、函式、類等在模組外是不可見的,除非它們被明確地用某種匯出形式匯出。相反,要使用從不同模組匯出的變數、函式、類、介面等,必須使用匯入的形式將其匯入。
9.2 非模組
在我們開始之前,重要的是要了解TypeScript認為什麼才是模組。JavaScript規範宣告,任何沒有export
或頂層 await(top-level await)的JavaScript檔案都應該被認為是一個指令碼而不是一個模組。
頂層await該特性可以讓 ES 模組對外表現為一個 async
函式,允許 ES 模組去 await
資料並阻塞其它匯入這些資料的模組。只有在資料確定並準備好的時候,匯入資料的模組才可以執行相應的程式碼。
在一個指令碼檔案中,變數和型別被宣告為在共享的全域性範圍內,並且假定你會使用outFile
編譯器選項將多個輸入檔案加入一個輸出檔案,或者在你的HTML中使用多個 <script>
標籤來載入這些檔案(順序正確!)。
如果你有一個目前沒有任何匯入或匯出的檔案,但你希望被當作一個模組來處理,請新增這一行:
export {}
這將改變該檔案,使其成為一個什麼都不輸出的模組。無論你的模組目標是什麼,這個語法都有效。
9.3 TypeScript中的模組
在TypeScript中編寫基於模組的程式碼時,有三個主要方面需要考慮:
- 語法:我想用什麼語法來匯入和匯出東西?
- 模組解析:模組名稱(或路徑)和磁碟上的檔案之間是什麼關係?
- 模組輸出目標:我編譯出來的JavaScript模組應該是什麼樣子的?
9.3.1 ES模組語法
一個檔案可以通過 export default
宣告一個主要出口:
// @filename: hello.ts
export default function helloWorld() {
console.log("Hello, world!");
}
然後通過以下方式匯入:
import hello from "./hello.js";
hello();
除了預設的匯出,你還可以通過省略 default
的 export
,實現有一個以上的變數和函式的匯出。
// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;
export class RandomNumberGenerator {}
export function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
這些可以通過 import
語法在另一個檔案中使用:
import { pi, phi, absolute } from "./maths.js";
console.log(pi);
// const absPhi: number
const absPhi = absolute(phi);
9.3.2 額外的匯入語法
可以使用 import {old as new}
這樣的格式來重新命名一個匯入:
import { pi as π } from "./maths.js";
// (alias)
var π: number
// import π
console.log(π);
你可以將上述語法混合並匹配到一個單一的import
中:
// @filename: maths.ts
export const pi = 3.14;
export default class RandomNumberGenerator {}
// @filename: app.ts
import RNGen, { pi as π } from "./maths.js";
// (alias) class RNGen
// import RNGen
RNGen;
// (alias) const π: 3.14
// import π
console.log(π);
你可以把所有匯出的物件,用 * as name
,把它們放到一個名稱空間:
// @filename: app.ts
import * as math from "./maths.js";
console.log(math.pi);
// const positivePhi: number
const positivePhi = math.absolute(math.phi);
你可以通過 import "./file "
匯入一個檔案,而不把任何變數納入你的當前模組:
// @filename: app.ts
import "./maths.js";
console.log("3.14");
在這種情況下, import
沒有任何作用。然而, maths.ts
中的所有程式碼都被解析了,這可能引發影響其他物件的副作用。
9.3.3 TypeScript特定的ES模組語法
型別可以使用與JavaScript值相同的語法進行匯出和匯入。
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export interface Dog {
breeds: string[];
yearOfBirth: number;
}
// @filename: app.ts
import { Cat, Dog } from "./animal.js";
type Animals = Cat | Dog;
TypeScript用兩個概念擴充套件了 import 語法,用於宣告一個型別的匯入。
import type
這是一個匯入語句,只能匯入型別:
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";
// @filename: valid.ts
import type { Cat, Dog } from "./animal.js";
export type Animals = Cat | Dog;
// @filename: app.ts
import type { createCatName } from "./animal.js";
const name = createCatName();
- 內聯型別匯入
TypeScript 4.5還允許以type為字首的單個匯入,以表明匯入的引用是一個型別:
// @filename: app.ts
import { createCatName, type Cat, type Dog } from "./animal.js";
export type Animals = Cat | Dog;
const name = createCatName();
9.3.4 ES模組語法與CommonJS行為
TypeScript有ES Module語法,它直接與CommonJS和AMD的 require
相關聯。使用ES Module的import
在大多數情況下與這些環境的 require
相同,但這種語法確保你在TypeScript檔案中與CommonJS的輸出有1對1的匹配:
import fs = require("fs")
const code = fs.readFileSync("hello.ts", "utf8")
9.4 CommonJS 語法
CommonJS是npm上大多數模組的交付格式。即使你使用上面的ES模組語法進行編寫,對CommonJS語法的工作方式有一個簡單的瞭解也會幫助你更容易地進行除錯。
9.4.1 匯出
識別符號是通過在一個全域性呼叫的 module
上設定 exports
屬性來匯出的。
function absolute(num: number) {
if (num < 0) return num * -1;
return num;
}
module.exports = {
pi: 3.14,
squareTwo: 1.41,
phi: 1.61,
absolute,
};
然後這些檔案可以通過 require
語句匯入:
const maths = require("maths");
// pi: any
maths.pi;
或者你可以使用JavaScript中的析構功能來簡化一下:
const { squareTwo } = require("maths");
// const squareTwo: any
squareTwo;
9.4.2 CommonJS和ES模組的互操作性
關於預設匯入和模組名稱空間物件匯入之間的區別,CommonJS和ES Modules之間存在著功能上的不匹配。
這個後面章節會詳細介紹。
9.5 TypeScript的模組解析選項
模組解析是指從 import
或 require
語句中獲取一個字串,並確定該字串所指的檔案的過程。
TypeScript包括兩種解析策略。經典和Node。當編譯器選項 module
不是 commonjs
時,經典策略是預設的,是為了向後相容。Node策略複製了Node.js在CommonJS模式下的工作方式,對 .ts 和 .d.ts 有額外的檢查。
在TypeScript中,有許多TSConfig標誌影響模組策略:moduleResolution
, baseUrl
, paths
, rootDirs
。
關於這些策略如何工作的全部細節,你可以參考《模組解析》。
9.6 TypeScript的模組輸出選項
有兩個選項會影響JavaScript輸出:
-
target
, 它決定了哪些JS功能被降級(轉換為在舊的JavaScript執行時執行),哪些保持不變 -
module
, 它決定了哪些程式碼用於模組之間的相互作用。
你使用的 target
是由你期望執行TypeScript程式碼的JavaScript執行時中的可用功能決定的。這可能是:你支援的最古老的網路瀏覽器,你期望執行的最低版本的Node.js,或者可能來自於你的執行時的獨特約束——比如Electron
.
所有模組之間的通訊都是通過模組載入器進行的,編譯器選項 module
決定使用哪一個。在執行時,模組載入器負責在執行一個模組之前定位和執行該模組的所有依賴項.
例如,這裡是一個使用ES模組語法的TypeScript檔案,展示了 module
的一些不同選項:
import { valueOfPi } from "./constants.js"
export const twoPi = valueOfPi * 2
- ES2020
import { valueOfPi } from "./constants.js"
export const twoPi = valueOfPi * 2
- CommonJS
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.twoPi = void 0;
const constants_js_1 = require("./constants.js");
exports.twoPi = constants_js_1.valueOfPi * 2;
- UMD
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./constants.js"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.twoPi = void 0;
const constants_js_1 = require("./constants.js");
exports.twoPi = constants_js_1.valueOfPi * 2;
});
請注意,ES2020實際上與原來的index.ts相同。
你可以在TSConfig 模組參考中看到所有可用的選項以及它們發出的JavaScript程式碼是什麼樣子。
9.7 TypeScript 名稱空間
TypeScript有自己的模組格式,稱為 名稱空間(namespaces)
,這比ES模組標準要早。這種語法對於建立複雜的定義檔案有很多有用的功能,並且在 DefinitelyTyped中仍然被積極使用。雖然沒有被廢棄,但名稱空間中的大部分功能都存在於ES Modules中,我們建議你使用它來與JavaScript的方向保持一致。 你可以在namespaces參考頁
中瞭解更多關於名稱空間的資訊。