目錄
- 4 類與面向對象:構建復雜的組件
- 4.1 類的定義與成員
- 4.2 繼承 (Inheritance)
- 4.3 接口實現 (Implements)
- 4.4 抽象類 (Abstract Class)
- 4.5 靜態成員 (Static Members)
- 5 更高級的類型:讓類型系統更靈活
- 5.1 聯合類型 (`|`)
- 5.2 交叉類型 (`&`)
- 5.3 字面量類型 (Literal Types)
- 5.4 枚舉 (`enum`)
- 5.5 `any` vs `unknown`
- 5.6 `void` 和 `never`
- 5.7 類型斷言 (`as` 或 `<Type>`)
- 5.8 練習
- 6 模塊化與工具鏈:組織代碼和提升效率
- 6.1 模塊 (`import` 和 `export`)
- 6.2 編譯模塊化的 TypeScript 代碼
- 6.3 模塊打包工具 (Bundlers)
- 6.4 練習
4 類與面向對象:構建復雜的組件
在復雜的智能家居系統中,我們不會把所有功能都寫在一起,而是會設計獨立的模塊,比如照明模塊、安防模塊、娛樂模塊等。每個模塊有自己的內部工作原理(屬性和方法),對外提供特定的操作接口。面向對象編程 (OOP) 和 TypeScript 的類就是用來實現這種模塊化設計的強大工具。
類 (Class) 是創建對象的藍圖或模板。它封裝了數據(屬性)和操作數據的方法。TypeScript 為 JavaScript 的類添加了類型系統和更強的可見性控制。
4.1 類的定義與成員
// 04.ts// 定義一個表示燈的類
class Light {// 屬性 (成員變量)// 默認是 public,表示可以在類的外部訪問brightness: number;// private 成員只能在類內部訪問,就像燈內部的電路細節不對外暴露private isOn: boolean;// protected 成員可以在類內部和其子類中訪問,就像一個家族內部的秘密配方protected maxBrightness: number = 100;// 只讀屬性,只能在構造函數中初始化readonly id: string;// 構造函數:在創建對象時執行,用于初始化屬性constructor(initialBrightness: number = 0, id: string) {this.brightness = initialBrightness;this.isOn = initialBrightness > 0;this.id = id;console.log(`燈 ${this.id} 已創建,初始亮度: ${this.brightness}`);}// 方法 (成員函數)// public 方法,可以在類的外部調用turnOn(): void {if (!this.isOn) {this.isOn = true;console.log(`燈 ${this.id} 打開了.`);this.brightness = 50; // 打開時設置一個默認亮度}}turnOff(): void {if (this.isOn) {this.isOn = false;console.log(`燈 ${this.id} 關閉了.`);this.brightness = 0;}}// private 方法,只能在類內部調用private checkStatus(): void {console.log(`燈 ${this.id} 當前狀態: ${this.isOn ? '開' : '關'}`);}// public 方法,內部調用 private 方法reportStatus(): void {this.checkStatus();console.log(`當前亮度: ${this.brightness}`);}// 訪問 protected 屬性get maxLevel(): number {return this.maxBrightness;}
}// 創建 Light 對象 (實例化)
const livingRoomLight = new Light(0, "LR1");
const kitchenLight = new Light(30, "K1");livingRoomLight.turnOn();
livingRoomLight.reportStatus(); // 調用 public 方法,內部使用了 private 方法// 嘗試訪問 private 或 protected 成員會報錯
// console.log(livingRoomLight.isOn); // 編譯時報錯:Property 'isOn' is private and only accessible within class 'Light'.
// console.log(livingRoomLight.maxBrightness); // 編譯時報錯:Property 'maxBrightness' is protected and only accessible within class 'Light' and its subclasses.// 嘗試修改 readonly 屬性會報錯 (在構造函數外)
// livingRoomLight.id = "new-id"; // 編譯時報錯:Cannot assign to 'id' because it is a read-only property.console.log("客廳燈最大亮度:", livingRoomLight.maxLevel); // 通過 public getter 訪問 protected 屬性
4.2 繼承 (Inheritance)
子類可以繼承父類的屬性和方法,并可以添加自己的新屬性或方法,或者覆蓋父類的方法。這就像在現有的基礎燈具(父類)上,開發出更高級的智能燈具(子類),它保留了基本功能,但也增加了顏色調節、定時開關等新功能。
// 04.ts (接著上面的代碼)// 定義一個繼承自 Light 的智能燈類
class SmartLight extends Light {color: string;constructor(initialBrightness: number = 0, id: string, initialColor: string = "白色") {// 調用父類的構造函數,必須是子類構造函數中的第一行super(initialBrightness, id);this.color = initialColor;console.log(`智能燈 ${this.id} 已創建,顏色: ${this.color}`);}// 子類可以添加新的方法setColor(color: string): void {this.color = color;console.log(`燈 ${this.id} 顏色設置為: ${this.color}`);}// 子類可以覆蓋父類的方法 (方法重寫)reportStatus(): void {// 可以調用父類的同名方法super.reportStatus();console.log(`當前顏色: ${this.color}`);// 子類可以訪問父類的 protected 屬性console.log(`智能燈最大亮度限制: ${this.maxBrightness}`);}// 嘗試訪問 private 成員會報錯// checkStatus() // 編譯時報錯:Property 'checkStatus' is private and only accessible within class 'Light'.
}const bedroomSmartLight = new SmartLight(10, "BR1", "藍色");
bedroomSmartLight.turnOn();
bedroomSmartLight.setColor("紅色");
bedroomSmartLight.reportStatus(); // 調用子類重寫后的方法
4.3 接口實現 (Implements)
接口不僅可以描述對象的形狀,還可以用來約束類的行為。一個類可以聲明它“實現”了某個接口,這意味著這個類必須提供接口中定義的所有屬性和方法。這就像一個智能設備必須遵循某個行業標準(接口),才能接入到整個智能家居系統中。
// 04.ts (接著上面的代碼)// 定義一個接口,描述可以控制電源的設備
interface PowerControllable {powerOn(): void;powerOff(): void;isOn(): boolean;
}// 定義一個表示智能插座的類,實現 PowerControllable 接口
class SmartOutlet implements PowerControllable {private deviceName: string;private status: boolean;constructor(name: string) {this.deviceName = name;this.status = false; // 默認關閉}powerOn(): void {if (!this.status) {this.status = true;console.log(`${this.deviceName} 已通電.`);}}powerOff(): void {if (this.status) {this.status = false;console.log(`${this.deviceName} 已斷電.`);}}isOn(): boolean {return this.status;}// 類可以有接口之外的其他成員getDeviceName(): string {return this.deviceName;}
}const deskFanOutlet = new SmartOutlet("臺扇插座");
deskFanOutlet.powerOn();
console.log(`${deskFanOutlet.getDeviceName()} 狀態: ${deskFanOutlet.isOn() ? '開' : '關'}`);
deskFanOutlet.powerOff();// 一個類可以實現多個接口
interface SettableTimer {setTimer(minutes: number): void;
}// 假設有一個既可以控制電源,又可以設置定時的智能設備
class SmartDevice implements PowerControllable, SettableTimer {// ... 實現 PowerControllable 和 SettableTimer 的所有方法deviceName: string;status: boolean;timerMinutes: number = 0;constructor(name: string) {this.deviceName = name;this.status = false;}powerOn(): void { /* ... */ console.log(`${this.deviceName} 通電`); this.status = true; }powerOff(): void { /* ... */ console.log(`${this.deviceName} 斷電`); this.status = false; }isOn(): boolean { return this.status; }setTimer(minutes: number): void {this.timerMinutes = minutes;console.log(`${this.deviceName} 設置定時 ${minutes} 分鐘`);}
}
4.4 抽象類 (Abstract Class)
抽象類不能直接實例化,只能作為其他類的父類。它可能包含抽象方法(只有方法簽名,沒有實現)和具體方法(有實現)。子類必須實現父類中的所有抽象方法。這就像一個“通用電器”的設計概念,它定義了所有電器都應該有的基本操作(比如打開/關閉),但具體如何實現這些操作(比如是燈亮還是風扇轉)由具體的子類(燈類、風扇類)來完成。
// 04.ts (接著上面的代碼)// 定義一個抽象的智能設備類
abstract class AbstractSmartDevice {abstract deviceType: string; // 抽象屬性,子類必須提供protected status: boolean = false;abstract turnOn(): void; // 抽象方法,子類必須實現abstract turnOff(): void; // 抽象方法,子類必須實現// 具體方法getStatus(): boolean {return this.status;}// 構造函數constructor(protected id: string) { // protected 參數屬性,會創建一個同名的 protected 屬性console.log(`抽象設備 ${this.id} 已創建`);}
}// 不能直接創建抽象類的實例
// const myDevice = new AbstractSmartDevice("abc"); // 編譯時報錯:Cannot create an instance of an abstract class.// 繼承抽象類并實現抽象成員
class ConcreteSmartLight extends AbstractSmartDevice {deviceType: string = "智能燈"; // 實現抽象屬性constructor(id: string, private brightness: number = 0) {super(id);}turnOn(): void { // 實現抽象方法this.status = true;this.brightness = 50;console.log(`智能燈 ${this.id} 打開,亮度 ${this.brightness}`);}turnOff(): void { // 實現抽象方法this.status = false;this.brightness = 0;console.log(`智能燈 ${this.id} 關閉`);}setBrightness(level: number): void {if (this.status) {this.brightness = level;console.log(`智能燈 ${this.id} 亮度設置為 ${this.brightness}`);} else {console.log(`智能燈 ${this.id} 已關閉,無法調節亮度`);}}
}const hallwayLight = new ConcreteSmartLight("H1");
hallwayLight.turnOn();
hallwayLight.setBrightness(80);
console.log("走廊燈狀態:", hallwayLight.getStatus());
hallwayLight.turnOff();
4.5 靜態成員 (Static Members)
類的屬性和方法默認是屬于類實例的(即創建對象后才能訪問)。但有時候,有些屬性或方法是屬于類本身,而不是任何特定的實例,可以使用 static
關鍵字。這就像智能家居系統的某個全局配置參數,或者一個用于創建所有設備實例的工廠方法,它們不依賴于某個具體的設備。
// 04.ts (接著上面的代碼)class DeviceManager {// 靜態屬性:所有設備共用的計數器static deviceCount: number = 0;// 靜態方法:用于注冊新設備static registerDevice(): void {DeviceManager.deviceCount++; // 在靜態方法中訪問靜態屬性console.log(`新設備已注冊,當前共有 ${DeviceManager.deviceCount} 個設備.`);}constructor() {DeviceManager.registerDevice(); // 在構造函數中調用靜態方法 (不太常見,但可行)}
}// 訪問靜態屬性和方法,無需創建類的實例
DeviceManager.registerDevice(); // 輸出:新設備已注冊,當前共有 1 個設備.
DeviceManager.registerDevice(); // 輸出:新設備已注冊,當前共有 2 個設備.console.log("總設備數:", DeviceManager.deviceCount); // 輸出:總設備數: 2// 創建類實例時也可以觸發靜態方法的調用 (如果構造函數里調用了)
const device1 = new DeviceManager(); // 輸出:新設備已注冊,當前共有 3 個設備.
const device2 = new DeviceManager(); // 輸出:新設備已注冊,當前共有 4 個設備.// 嘗試通過實例訪問靜態成員會報錯
// console.log(device1.deviceCount); // 編譯時報錯:Property 'deviceCount' is a static member of type 'DeviceManager'.
// device1.registerDevice(); // 編譯時報錯:Property 'registerDevice' is a static member of type 'DeviceManager'.
編譯 04.ts
:
tsc --project 04-typescript/tsconfig.json
會生成 04.js
文件。然后在 HTML 中引入 04.js
。
執行后的效果
小結: 類和面向對象編程是構建復雜應用的重要范式。TypeScript 為類添加了強大的類型和可見性控制,支持繼承和接口實現,使得代碼結構更清晰,更易于管理和擴展,就像有了更標準化的智能家居模塊。
練習:
- 定義一個基類
Vehicle
,包含屬性speed
(number) 和方法move()
。 - 創建一個子類
Car
繼承自Vehicle
,添加屬性brand
(string),并重寫move()
方法,使其輸出汽車正在移動。 - 定義一個接口
Drawable
,包含方法draw(): void
。 - 創建一個類
Circle
,實現Drawable
接口,并在draw
方法中輸出繪制圓形的信息。 - 修改
Light
類,使其brightness
屬性默認是protected
。創建一個繼承自Light
的DimmingLight
類,添加一個setBrightness(level: number)
方法,該方法可以訪問父類的brightness
屬性來調節亮度。
5 更高級的類型:讓類型系統更靈活
TypeScript 的類型系統非常靈活,不僅限于基本類型和固定結構。它提供了多種方式來描述數據可能存在的多種狀態或組合,這就像智能家居系統中的一個設備可能有多重模式(聯合類型),或者需要同時滿足多個功能規范(交叉類型)。
5.1 聯合類型 (|
)
聯合類型表示一個變量可以持有多種類型中的任意一種。這就像一根電線,有時候用來傳輸數據信號,有時候用來傳輸控制信號,但它總歸是這兩種中的一種。
// 05.ts// 一個變量可以是 string 或 number
let status: string | number;status = "在線"; // 正確
status = 1; // 正確// status = true; // 編譯時報錯:Type 'boolean' is not assignable to type 'string | number'.// 函數參數也可以是聯合類型
function printId(id: number | string): void {console.log(`ID: ${id}`);// 在使用聯合類型的變量時,需要進行類型檢查 (類型縮小)if (typeof id === 'string') {// 在這個塊里,id 被縮小為 string 類型console.log(id.toUpperCase());} else {// 在這個塊里,id 被縮小為 number 類型console.log(id.toFixed(2)); // number 類型的方法}
}printId(101);
printId("abc-123");
// printId(true); // 編譯時報錯
5.2 交叉類型 (&
)
交叉類型表示一個類型是多種類型的組合,它必須同時滿足所有類型的要求。這就像一個設備既是照明設備,又是安防設備,它必須同時具備照明和安防的所有功能。
// 05.ts (接著上面的代碼)interface Switchable {turnOn(): void;turnOff(): void;
}interface Dimmable {setBrightness(level: number): void;
}// 交叉類型:同時具備 Switchable 和 Dimmable 的能力
type SmartLamp = Switchable & Dimmable;// 創建一個對象,它必須實現 Switchable 和 Dimmable 的所有方法
const mySmartLamp: SmartLamp = {turnOn: () => console.log("燈打開"),turnOff: () => console.log("燈關閉"),setBrightness: (level) => console.log(`亮度設置為 ${level}`)
};mySmartLamp.turnOn();
mySmartLamp.setBrightness(75);
mySmartLamp.turnOff();
5.3 字面量類型 (Literal Types)
字面量類型允許你指定變量的值只能是一個特定的字符串、數字或布爾值。這就像一個開關,它的狀態只能是“開”或“關”,不能是其他任何值。
// 05.ts (接著上面的代碼)// 變量的值只能是 "success", "error", "loading" 中的一個
let apiStatus: "success" | "error" | "loading";apiStatus = "success"; // 正確
apiStatus = "loading"; // 正確// apiStatus = "done"; // 編譯時報錯:Type '"done"' is not assignable to type '"success" | "error" | "loading"'.// 函數參數或返回值也可以是字面量類型
function setDirection(direction: "up" | "down" | "left" | "right"): void {console.log(`移動方向: ${direction}`);
}setDirection("up");
// setDirection("forward"); // 編譯時報錯
5.4 枚舉 (enum
)
枚舉允許你定義一組命名的常量。這對于表示一組相關的、有限的取值非常有用,比如星期幾、交通燈顏色、訂單狀態等。它們讓代碼更易讀。這就像給智能家居系統的各種工作模式定義了清晰的名稱,而不是用數字或字符串的魔術值。
// 05.ts (接著上面的代碼)// 數字枚舉 (默認從 0 開始)
enum Direction {Up, // 0Down, // 1Left, // 2Right // 3
}let playerDirection: Direction = Direction.Up;
console.log("玩家方向 (數字):", playerDirection); // 輸出 0
console.log("玩家方向 (名稱):", Direction[playerDirection]); // 輸出 "Up"// 可以指定起始值
enum StatusCode {Success = 200,NotFound = 404,InternalError = 500
}let responseStatus: StatusCode = StatusCode.Success;
console.log("響應狀態碼:", responseStatus); // 輸出 200// 字符串枚舉 (推薦,可讀性更好)
enum ApiStatus {Fetching = "FETCHING",Success = "SUCCESS",Error = "ERROR"
}let currentApiStatus: ApiStatus = ApiStatus.Fetching;
console.log("當前API狀態:", currentApiStatus); // 輸出 "FETCHING"
5.5 any
vs unknown
any
: 表示可以是任何類型。使用any
會關閉 TypeScript 的類型檢查,回到原生 JavaScript 的狀態。謹慎使用,它破壞了 TypeScript 提供的安全性。就像一根沒有標簽的電線,你不知道它是傳輸什么信號的,使用時非常危險。unknown
: 表示未知類型。與any
不同的是,使用unknown
類型的變量之前,你必須先進行類型檢查或類型斷言,才能對其進行操作。這就像一根你不知道用途的電線,在使用它之前,你必須先用儀表測試清楚它的類型。
// 05.ts (接著上面的代碼)let data: any = 123;
data = "hello";
data = true;
data.toFixed(2); // 不會報錯,any 類型不做檢查
data.toUpperCase(); // 不會報錯,any 類型不做檢查let dataUnknown: unknown = "hello world";// 嘗試直接對 unknown 類型進行操作會報錯
// dataUnknown.toUpperCase(); // 編譯時報錯:'dataUnknown' is of type 'unknown'.// 必須先進行類型檢查或斷言
if (typeof dataUnknown === 'string') {console.log(dataUnknown.toUpperCase()); // 在這個塊里,dataUnknown 被縮小為 string
}// 或者使用類型斷言 (后面會講)
// console.log((dataUnknown as string).toUpperCase());
5.6 void
和 never
void
: 表示函數沒有返回值。這是最常見的,比如只執行某個操作而不返回結果的函數。never
: 表示函數永遠不會返回結果。這通常用于會拋出錯誤或包含無限循環的函數。這就像一個警報系統,一旦觸發就進入一個不可停止的狀態。
// 05.ts (接著上面的代碼)// 沒有返回值的函數
function logAction(action: string): void {console.log(`執行動作: ${action}`);
}logAction("初始化系統");// 永遠不會返回的函數 (例如,拋出錯誤)
function throwError(message: string): never {throw new Error(message);
}// 永遠不會返回的函數 (例如,無限循環)
// function infiniteLoop(): never {
// while (true) {
// // ...
// }
// }// 調用會拋出錯誤的函數
// try {
// throwError("出錯了!");
// } catch (e) {
// console.error("捕獲到錯誤:", e.message);
// }
5.7 類型斷言 (as
或 <Type>
)
有時候,你比 TypeScript 更清楚一個變量的實際類型。類型斷言就是告訴編譯器,“相信我,這個變量就是這個類型”。這就像你看到一根沒有標簽的電線,但根據經驗你確定它是電源線,你就可以“斷言”它是電源線。
重要提示: 類型斷言只在編譯階段起作用,不會改變變量的實際運行時類型或執行任何額外的檢查。如果斷言錯誤,可能會導致運行時錯誤。謹慎使用!
有兩種語法:
value as Type
(JSX 中推薦使用)<Type>value
(在.tsx
文件中與 JSX 語法沖突,不推薦)
// 05.ts (接著上面的代碼)let someValue: unknown = "這是個字符串";// 使用 as 進行類型斷言
let strLength1: number = (someValue as string).length;
console.log("字符串長度 (as):", strLength1);// 使用 <Type> 進行類型斷言 (在 tsx 文件中可能與 JSX 沖突)
let strLength2: number = (<string>someValue).length;
console.log("字符串長度 (<Type>):", strLength2);// 危險的斷言:如果 someValue 實際上不是字符串
let anotherValue: unknown = 123;
// let dangerousLength: number = (anotherValue as string).length; // 編譯時不會報錯,但運行時會出錯!// 獲取 DOM 元素時經常用到類型斷言
// const element = document.getElementById("my-canvas") as HTMLCanvasElement;
// // 現在 TypeScript 知道 element 是一個 HTMLCanvasElement 類型,可以使用其特有的屬性和方法
// const ctx = element.getContext("2d");
編譯 05.ts
:
tsc --project 04-typescript/tsconfig.json
會生成 05.js
文件。然后在 HTML 中引入 05.js
。
執行后的效果
小結: 聯合類型、交叉類型、字面量類型、枚舉等高級類型使得 TypeScript 能夠更精確地描述復雜的數據結構和取值范圍。理解 any
和 unknown
的區別以及何時使用類型斷言是編寫安全 TypeScript 代碼的關鍵。
5.8 練習
- 定義一個聯合類型
ContactInfo
,可以是string
(郵箱) 或number
(電話號碼)。聲明一個變量myContact
為此類型,并分別賦值一個郵箱地址和電話號碼。 - 定義一個接口
HasArea
,包含方法getArea(): number
。定義一個接口HasPerimeter
,包含方法getPerimeter(): number
。定義一個交叉類型Shape
,它是HasArea
和HasPerimeter
的組合。創建一個對象,實現Shape
類型。 - 定義一個字符串字面量類型
TrafficLightColor
,它的值只能是"Red"
,"Yellow"
,"Green"
中的一個。編寫一個函數changeLight(color: TrafficLightColor): void
。 - 定義一個數字枚舉
LogLevel
,包含Debug = 0
,Info = 1
,Warning = 2
,Error = 3
。編寫一個函數logMessage(level: LogLevel, message: string): void
,根據日志級別輸出信息。
6 模塊化與工具鏈:組織代碼和提升效率
隨著項目代碼量的增加,我們不可能把所有代碼都放在一個文件里。模塊化是將代碼分割成獨立、可復用的文件的方式,這就像智能家居系統中的各個房間都有獨立的電路箱和布線,互不干擾但又能通過主干線連接。TypeScript 完全支持 ES Modules (import
/export
)。
同時,TypeScript 的強大之處在于它的編譯器 tsc
和豐富的配置選項,以及與現代前端工具鏈的集成。
6.1 模塊 (import
和 export
)
在 TypeScript 中,每個 .ts
文件默認就是一個模塊。你可以使用 export
關鍵字導出模塊中的變量、函數、類、接口等,然后使用 import
關鍵字在其他模塊中引入它們。
創建兩個文件:utils.ts
和 main.ts
。
utils.ts
:
// utils.ts - 一個工具函數模塊// 導出常量
export const PI = 3.14159;// 導出函數
export function add(x: number, y: number): number {return x + y;
}// 導出接口
export interface Config {apiUrl: string;timeout: number;
}// 導出類
export class Logger {log(message: string): void {console.log(`[日志] ${message}`);}
}// 默認導出 (一個模塊只能有一個默認導出)
export default class AppSettings {theme: string = 'light';
}
main.ts
:
// main.ts - 主應用模塊// 導入單個或多個導出
import { PI, add, Logger, Config } from './utils'; // 注意文件后綴省略// 導入默認導出 (可以取任意名字)
import Settings from './utils'; // 注意這里沒有大括號// 導入模塊中的所有導出,并將其放入一個命名空間對象中
import * as Utils from './utils';console.log("PI:", PI);
console.log("2 + 3 =", add(2, 3));const appLogger = new Logger();
appLogger.log("應用啟動");const appConfig: Config = {apiUrl: "http://api.example.com",timeout: 5000
};
console.log("應用配置:", appConfig);const settings = new Settings();
console.log("應用設置主題:", settings.theme);// 訪問命名空間中的導出
console.log("Utils.PI:", Utils.PI);
6.2 編譯模塊化的 TypeScript 代碼
直接使用 tsc main.ts
編譯可能會遇到問題,因為瀏覽器默認不支持 ES Modules 語法(需要特定的 <script type="module">
標簽,并且文件路徑和擴展名有要求)。更常見的做法是使用 tsconfig.json
配置文件來管理整個項目的編譯設置。
tsconfig.json
文件:
這個文件位于項目的根目錄,用來告訴 tsc
編譯器如何編譯你的 TypeScript 項目。
初始化 tsconfig.json
: 在項目根目錄運行命令:
tsc --init
這會生成一個帶有大量注釋的 tsconfig.json
文件。一些重要的配置項:
"compilerOptions"
: 編譯選項的核心配置對象。"target"
: 指定編譯后的 JavaScript 版本 (如 “es5”, “es6”, “es2020” 等)。"module"
: 指定生成的模塊系統 (如 “commonjs”, “es2015”, “esnext” 等)。對于現代 Web 開發,“es2015” 或 “esnext” 配合模塊打包工具更常用。"outDir"
: 指定編譯后 JavaScript 文件的輸出目錄。"rootDir"
: 指定 TypeScript 源文件的根目錄。"strict"
: 啟用所有嚴格的類型檢查選項(強烈推薦開啟!)。"esModuleInterop"
: 允許使用 ES Modules 語法導入 CommonJS 模塊。"forceConsistentCasingInFileNames"
: 強制文件名大小寫一致。"skipLibCheck"
: 跳過聲明文件的類型檢查,提高編譯速度。
"include"
: 指定需要編譯的文件或目錄(默認是當前目錄及子目錄下的所有.ts
文件)。"exclude"
: 指定不需要編譯的文件或目錄(如node_modules
)。
修改 tsconfig.json
示例:
{"compilerOptions": {"target": "es2018", // 編譯到較新版本的 JS,現代瀏覽器支持"module": "esnext", // 使用最新的 ES Module 語法"outDir": "./dist", // 編譯輸出到 dist 目錄"rootDir": "./src", // TypeScript 源文件在 src 目錄"strict": true, // 開啟所有嚴格檢查"esModuleInterop": true,"forceConsistentCasingInFileNames": true,"skipLibCheck": true},"include": ["src/**/*.ts" // 包含 src 目錄下所有子目錄中的 .ts 文件],"exclude": ["node_modules"]
}
現在,將你的 utils.ts
和 main.ts
文件移到 src
目錄下。然后在項目根目錄運行 tsc
命令(不帶文件名):
tsc
tsc
命令會自動查找 tsconfig.json
文件并按照其中的配置編譯項目。編譯后的 .js
文件會輸出到 ./dist
目錄。
6.3 模塊打包工具 (Bundlers)
雖然 tsc
可以編譯 TypeScript 模塊,但生成的 ES Module 文件在瀏覽器中直接使用可能需要 <script type="module">
標簽,并且處理復雜的依賴關系(比如導入 node_modules
中的庫)并不方便。
現代前端開發通常會使用模塊打包工具 (Bundlers),如 Webpack, Parcel, Vite 等。這些工具可以處理模塊間的依賴關系,將多個模塊打包成一個或少數幾個優化過的 JavaScript 文件,并通常內置或容易配置對 TypeScript 的支持。
模塊打包工具會讀取你的入口文件(例如 dist/main.js
),分析其依賴關系,然后將所有需要的代碼打包到一個或多個文件中。
基本打包流程(概念性):
- 你編寫
.ts
文件,使用import
/export
。 - 使用
tsc
編譯.ts
文件到.js
文件(位于outDir
)。 - 使用打包工具(如 Webpack)讀取
outDir
中的入口.js
文件。 - 打包工具分析依賴,將所有相關的
.js
文件整合成最終用于瀏覽器的文件(通常也包含代碼優化、壓縮等)。 - 在 HTML 中只引入打包后的文件。
很多現代打包工具(如 Vite)甚至可以直接處理 .ts
文件,無需先用 tsc
編譯到單獨的 .js
目錄,簡化了流程。理解 tsconfig.json
仍然非常重要,因為它控制了 TypeScript 的編譯行為和類型檢查規則。
小結: 模塊化是組織大型 TypeScript 項目的關鍵。ES Modules (import
/export
) 是標準的模塊化方案。tsconfig.json
文件是 TypeScript 項目的配置中心,控制著編譯器的行為。模塊打包工具是現代前端開發中處理模塊依賴和優化代碼不可或缺的一部分。
6.4 練習
- 創建一個新的項目目錄。
- 在目錄中運行
npm init -y
初始化一個 Node.js 項目。 - 安裝 TypeScript:
npm install typescript --save-dev
(或者全局安裝npm install -g typescript
)。 - 運行
tsc --init
生成tsconfig.json
文件。 - 創建
src
目錄,并在其中創建calculator.ts
和app.ts
文件。 - 在
calculator.ts
中導出一個函數multiply(a: number, b: number): number
和一個常量E = 2.71828
。 - 在
app.ts
中導入multiply
和E
,使用它們進行計算并在控制臺輸出結果。 - 修改
tsconfig.json
,設置outDir
為dist
,rootDir
為src
,target
為es2018
,module
為esnext
,并開啟strict
。 - 在項目根目錄運行
tsc
命令,查看dist
目錄下生成的.js
文件。 - 創建一個簡單的
index.html
文件,引入dist/app.js
(使用<script type="module" src="dist/app.js"></script>
),在瀏覽器中查看控制臺輸出。