- 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參考頁
中了解更多關於命名空間的信息。