← Programming Core

TYPESCRIPT CORE · 92

Object、Interface、Union:把 Domain Model 與狀態寫清楚

上一課只知道「外部資料未知」。這一課開始定義應用程式內部接受的合法資料形狀,讓不可能狀態更難被表示。

Learning outcomes

1. Domain model:Course 應該長什麼樣?

interface Course {
  readonly id: number;
  title: string;
  score: number | null;
}

readonly id 表示 application code 不應重新指定 id;score 明確允許 null,而不是假裝每門課一定已經有成績。

2. Optional 跟 nullable 不是同一件事

type A = {
  score?: number;
};

type B = {
  score: number | null;
};
型別意思
score?: numberproperty 可以根本不存在
score: number | nullproperty 必須存在,但 value 可能是 null

3. Literal union:不要讓 status 是任意 string

type Status =
  | "idle"
  | "loading"
  | "success"
  | "error";

如果只是 string,"sucess" 這種 typo 也合法;literal union 把合法值集合縮小。

4. Discriminated union:讓不同狀態帶不同資料

type CourseState =
  | { kind: "idle" }
  | { kind: "loading" }
  | { kind: "success"; data: Course }
  | { kind: "error"; message: string };

現在 success 一定有 data,error 一定有 message。這比:

type BadState = {
  loading: boolean;
  data?: Course;
  error?: string;
};

更不容易出現「loading=true、data 有值、error 也有值」這種矛盾狀態。

5. Project checkpoint:Typed Course API Client v2

let state: CourseState = { kind: "idle" };

async function refreshCourse(id: number) {
  state = { kind: "loading" };

  try {
    const raw = await loadCourse(id);

    // 下一課會真正 validate raw
    state = {
      kind: "success",
      data: raw as Course
    };
  } catch (error) {
    state = {
      kind: "error",
      message: "Unable to load course"
    };
  }
}
刻意留下的問題:raw as Course 只是 type assertion,不是 validation。下一課就是把這個洞補起來。

6. type vs interface:先會讀,不要陷入宗教戰爭

兩者在 object shape 上能力高度重疊。初學最重要的是能理解團隊 code,而不是先建立「永遠只用哪一種」的規則。

Knowledge check

  1. 為什麼 score?: number 與 score: number | null 不等價?
  2. 把 CourseState 增加 empty 狀態。
  3. 列出 BadState 能表示但其實不合理的兩種組合。
  4. 解釋 as Course 為什麼不能取代 runtime validation。