在上一篇文章中,我們了解了 TypeScript 的概念和優勢。今天我們來深入探討 TypeScript 的核心——類型系統。
TypeScript 代碼最明顯的特徵,就是為 JavaScript 變量加上了類型聲明。
let foo: string;
上面示例中,變量 foo
的後面使用冒號,聲明了它的類型為 string
。
類型聲明的寫法,一律為在標識符後面添加「冒號 + 類型」。函數參數和返回值,也是這樣來聲明類型。
function toString(num: number): string {
return String(num);
}
上面示例中,函數 toString()
的參數 num
的類型是 number
。參數列表的圓括號後面,聲明了返回值的類型是 string
。
變量的值與聲明的類型如果不一致,TypeScript 就會報錯。
let name: string = "Alice";
name = 123; // 錯誤!不能將數字賦值給字符串類型
另外,TypeScript 規定,變量只有賦值後才能使用,否則就會報錯。
let x: number;
console.log(x); // 報錯:在賦值前使用了變量 'x'
上面示例中,變量 x
沒有賦值就被讀取,導致報錯。而 JavaScript 允許這種行為,不會報錯,沒有賦值的變量會返回 undefined
。
TypeScript 提供了豐富的基本類型,讓我們來看看最常用的幾種:
// 字符串類型
let name: string = "Alice";
let message: string = `Hello, ${name}!`;
// 數字類型
let age: number = 25;
let price: number = 99.99;
let binary: number = 0b1010; // 二進制
let hex: number = 0xff; // 十六進制
// 布林類型
let isStudent: boolean = true;
let isCompleted: boolean = false;
// undefined 和 null
let undefinedValue: undefined = undefined;
let nullValue: null = null;
// 數字陣列
let numbers: number[] = [1, 2, 3, 4, 5];
// 字符串陣列
let names: string[] = ["Alice", "Bob", "Charlie"];
// 另一種寫法(泛型語法)
let scores: Array<number> = [85, 92, 78];
let cities: Array<string> = ["台北", "台中", "高雄"];
// 基本物件類型
let person: { name: string; age: number } = {
name: "Alice",
age: 25
};
// 可選屬性(使用 ? 標記)
let student: { name: string; age?: number } = {
name: "Bob"
// age 是可選的,可以不提供
};
// 只讀屬性(使用 readonly 標記)
let config: { readonly apiUrl: string; timeout: number } = {
apiUrl: "https://api.example.com",
timeout: 5000
};
// config.apiUrl = "new url"; // 錯誤!不能修改只讀屬性
類型聲明並不是必需的,如果沒有,TypeScript 會自己推斷類型。
let foo = 123; // TypeScript 推斷 foo 的類型為 number
上面示例中,變量 foo
並沒有類型聲明,TypeScript 就會推斷它的類型。由於它被賦值為一個數值,因此 TypeScript 推斷它的類型為 number
。
後面,如果變量 foo
更改為其他類型的值,跟推斷的類型不一致,TypeScript 就會報錯。
let foo = 123; // 推斷為 number
foo = 'hello'; // 報錯:不能將字符串賦值給數字類型
// 聲明時推斷
let message = "Hello"; // 推斷為 string
let count = 0; // 推斷為 number
let items = [1, 2, 3]; // 推斷為 number[]
// 函數返回值推斷
function add(a: number, b: number) {
return a + b; // 推斷返回值為 number
}
// 上下文推斷
let colors = ["red", "green", "blue"]; // 推斷為 string[]
colors.push("yellow"); // 正確
// colors.push(123); // 錯誤!
TypeScript 也可以推斷函數的返回值。
function toString(num: number) {
return String(num); // 推斷返回值為 string
}
上面示例中,函數 toString()
沒有聲明返回值的類型,但是 TypeScript 推斷返回的是字符串。正是因為 TypeScript 的類型推斷,所以函數返回值的類型通常是省略不寫的。
從這裡可以看到,TypeScript 的設計思想是,類型聲明是可選的,你可以加,也可以不加。即使不加類型聲明,依然是有效的 TypeScript 代碼,只是這時不能保證 TypeScript 會正確推斷出類型。由於這個原因,所有 JavaScript 代碼都是合法的 TypeScript 代碼。
這樣設計還有一個好處,將以前的 JavaScript 項目改為 TypeScript 項目時,你可以逐步地為老代碼添加類型,即使有些代碼沒有添加,也不會無法運行。
聯合類型(Union Types)表示一個值可以是幾種類型之一。使用 |
符號分隔每個類型。
// 基本聯合類型
let id: string | number;
id = "abc123"; // 正確
id = 123; // 正確
// id = true; // 錯誤!boolean 不在聯合類型中
// 函數參數使用聯合類型
function formatId(id: string | number): string {
// 需要進行類型檢查
if (typeof id === "string") {
return id.toUpperCase();
} else {
return id.toString();
}
}
// 陣列元素的聯合類型
let mixedArray: (string | number)[] = ["hello", 123, "world", 456];
當使用聯合類型時,TypeScript 只能訪問聯合類型中所有類型共有的成員。要訪問特定類型的成員,需要進行類型檢查:
function processValue(value: string | number) {
// 這樣會報錯,因為 number 沒有 toUpperCase 方法
// return value.toUpperCase();
// 正確的做法:類型縮小
if (typeof value === "string") {
return value.toUpperCase(); // 在這個分支中,value 被縮小為 string
} else {
return value.toFixed(2); // 在這個分支中,value 被縮小為 number
}
}
類型別名(Type Aliases)允許我們為類型創建新的名稱。使用 type
關鍵字來定義:
// 基本類型別名
type UserID = string | number;
type Status = "pending" | "completed" | "failed";
// 使用類型別名
let userId: UserID = "user123";
let currentStatus: Status = "pending";
// 物件類型別名
type User = {
id: UserID;
name: string;
email: string;
status: Status;
};
let user: User = {
id: "u001",
name: "Alice",
email: "alice@example.com",
status: "completed"
};
// 函數類型別名
type EventHandler = (event: string) => void;
function addEventListener(event: string, handler: EventHandler) {
// 實作邏輯
}
學習 TypeScript 需要分清楚「值」(value)和「類型」(type)。
「類型」是針對「值」的,可以視為是後者的一個元屬性。每一個值在 TypeScript 裡面都是有類型的。比如,3
是一個值,它的類型是 number
。
// 這是值代碼(JavaScript 語法)
let message = "Hello World";
console.log(message);
// 這是類型代碼(TypeScript 語法)
let message: string;
// 編譯後,類型代碼會被移除,只保留值代碼
let message = "Hello World";
console.log(message);
TypeScript 代碼只涉及類型,不涉及值。所有跟「值」相關的處理,都由 JavaScript 完成。
這一點務必牢記。TypeScript 項目裡面,其實存在兩種代碼,一種是底層的「值代碼」,另一種是上層的「類型代碼」。前者使用 JavaScript 語法,後者使用 TypeScript 的類型語法。
它們是可以分離的,TypeScript 的編譯過程,實際上就是把「類型代碼」全部拿掉,只保留「值代碼」。
// 在類型空間中
type Point = { x: number; y: number };
// 在值空間中
const point = { x: 10, y: 20 };
// 函數可以同時存在於兩個空間
function distance(p1: Point, p2: Point): number { // Point 在類型空間
return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
}
// distance 函數本身存在於值空間
const calculateDistance = distance;
編寫 TypeScript 項目時,不要混淆哪些是值代碼,哪些是類型代碼。
TypeScript 官方提供了線上編輯器 TypeScript Playground,這是學習和測試 TypeScript 的好東西。