iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0
佛心分享-IT 人自學之術

欸欸!! 這是我的學習筆記系列 第 15

Day15 - TypeScript (2) - 類型基礎

  • 分享至 

  • xImage
  •  

前言

在上一篇文章中,我們了解了 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];

類型縮小(Type Narrowing)

當使用聯合類型時,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 的編譯過程,實際上就是把「類型代碼」全部拿掉,只保留「值代碼」。

類型空間 vs 值空間

// 在類型空間中
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 Playground

TypeScript 官方提供了線上編輯器 TypeScript Playground,這是學習和測試 TypeScript 的好東西。

主要功能:

  • 即時編譯:左側輸入 TypeScript,右側即時顯示編譯後的 JavaScript
  • 錯誤提示:即時顯示類型錯誤和語法錯誤
  • 版本選擇:可以選擇不同的 TypeScript 版本進行測試
  • 分享代碼:可以生成連結分享代碼片段
  • 範例庫:內建豐富的範例代碼

上一篇
Day14 - TypeScript (1) - 基本介紹
下一篇
Day16 - TypeScript (3) - 開發環境與工具
系列文
欸欸!! 這是我的學習筆記16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言