這篇文章需要閱讀者對於 TypeScript (以下簡稱 TS) 及 JavaScript (以下簡稱 JS) 的語法有基礎的認知,入門可以參考 TS 官網的 Get Started 並選擇適合你的章節開始閱讀。

這篇文章中 Generics、Error Type in Catch 和 Utility Types 是 Matt Pocock 超讚的 TS 教學 - Total TypeScript ,在 Beginner’s TypeScript 課程中有用到的。這些範例為了方便我有精簡修改程式碼來說明,完整程式邏輯可以看 Matt 的原始教學,有提供可以互動的範例唷 (透過像 Jest 測試的方式)。

Generics

可以給 constructor function 加上 <type parameter> 來定義可加入的值 (原教學中用 Set 做例子),這邊給進 new Map() 的是要求 <key, value> 的型別:TS Playground

const beverageOrder = new Map<string, number>();

beverageOrder.set('coffee', 1);
// @ts-expect-error
beverageOrder.set(1, 'coffee');

Error Type In Catch

Try & catch 中,catch 的 e 參數是什麼型別呢?TS Playground

const resStatus = {
    "200": "okay",
    "404": "not found"
}

function setResult(code: string) {
    try {
        if (code === "404") {
            throw new Error(resStatus[404]);
        }
    } catch(err) {
          // error
        return err.message;
    }
    return resStatus[200];
}

上面這段程式碼會在 err 報錯:Object is of type ‘unknown’.

那試著給 err 指定類型:

// error
catch(err: Error) {
    return err.message
}

這次報錯在 Error 型別身上:Catch clause variable type annotation must be ‘any’ or ‘unknown’ if specified.

Okay,所以現在知道 err 的型別要給 ‘any’ 或 ‘unknown’,前面已知 unknown 會報錯,所以選 any,解決,thank AnyScript!

其他可能的解法如:

catch(err) {
    return (err as Error).message
}

或是

catch(err) {
    if (err instanceof Error) {
        return err.message
    }
}

Utility Types

全部的 utility types 可以在 TS 官網的 Utility Types 文件看到。

Promise

TS Playground 這段 code 是在 person 這個變數做型別宣告,讓 TS 知道它是 LukeSkywalker

interface LukeSkywalker {
  [characteristic: string]: string
}

const fetchLukeSkywalker = async () => {
  const data = await fetch("https://swapi.dev/api/people/1");
  const person: LukeSkywalker = await data.json();
  return person;
};

滑鼠移到 fetchLukeSkywalker 就會顯示:const fetchLukeSkywalker: () => Promise<LukeSkywalker>;就也可以寫成,但指去 person 就會顯示它是 any

const fetchLukeSkywalker = async (): Promise<LukeSkywalker> => {
  const data = await fetch("https://swapi.dev/api/people/1");
  const person = await data.json();
  return person;
};

Record

在 Generics 提到的 new Map<key, value>,如果想要對也是 key-value 配對的 object 使用,可以用 Record<key, value>

const beverageOrder: Record<string, number> = {};

beverageOrder['coffee'] = 1;
// @ts-expect-error;
beverageOrder[1] = 'coffee';
  • 這個 utility 的應用還可以參考官網例子,更靈活:TypeScript Doc

Pick

有時,我們只想要 interface 中特定幾個 properties 來產生一個新 type,就可以用 Pick<Type, Keys>TS Playground

interface Meal {
    "omelette": number,
    "coffee": number,
    "bagal": number
}

type Food = Pick<Meal, "omelette" | "bagal">

Omit

Pick 中提到的狀況,如果換個方向,也就是我們不想要 interface 中特定的元素(們),來產生一個新的 type,就可以用 Omit<Type, Keys>TS Playground

interface Meal {
    "omelette": number,
    "coffee": number,
    "bagal": number
}

type Food = Omit<Meal, "coffee">

Inferring Within Conditional Types

標題就是官網介紹 infer 這個 keyword 段落的的標題 - Inferring Within Conditional Types;就我淺白的理解 infer 是用來伸手到 function 裡去指(infer),拿取我們想要的元素用的,要用它有幾個條件:

看例子應該比較好懂,LogRocket Blog 的 Understanding infer in TypeScript 裡講解了整個情境的前因後果,順便帶到了 utility types 中的 Extract、Exclude,和 ReturnType 的實踐概念就是跟 infer 有關。

借用文章中開頭提的例子來看:TS Playground

function describePerson(person: {
  name: string;
  age: number;
  hobbies: [string, string];
}) {
  return `${person.name} is ${person.age} years old and love ${person.hobbies.join(" and  ")}.`;
}

const alex = {
  name: 'Alex',
  age: 20,
  hobbies: ['walking', 'cooking']
}
// error: Type 'string[]' is not assignable to type '[string, string]'.
describePerson(alex)

這例子有個假設,這個 function describePerson,它的參數 person,只會出現在這個函式中,沒有必要為了它獨立寫一個 type (當然要寫也可以)。這樣做就不會報錯:

type Person = {
  name: string;
  age: number;
  hobbies: [string, string];
}

function describePerson(person: Person) {
  return `${person.name} is ${person.age} years old and love ${person.hobbies.join(" and  ")}.`;
}

const alex: Person = {
  name: 'Alex',
  age: 20,
  hobbies: ['walking', 'cooking']
}

describePerson(alex)

那麼使用 infer 的話:TS Playground

  1. type ParameterType<T> 用來提取 describePerson 的參數出來當成一個 type 指派給 alex,這就是我們的目標。

  2. 等式的右邊,type parameter T 是之後要放 function type 的,也就是 typeof describePerson

  3. extends 後面是描繪一個函式的形狀,因為 function return 不是我們的目標,所以寫個 any,目標是函式的參數 (person: infer PersonType)infer 就是為了指 person 的型別定義,這邊給它一個變數 PersonType

  4. 藉由 ternary operator,當 T 符合 extends 後面這個函式的形狀時,讓它回傳 PersonType 出來,這樣我們就能拿到函式的參數型別了。

function describePerson(person: {
  name: string;
  age: number;
  hobbies: [string, string];
}) {
  return `${person.name} is ${person.age} years old and love ${person.hobbies.join(" and  ")}.`;
}

type ParameterType<T> = T extends (person: infer PersonType) => any ? PersonType : never;
type DescribePersonParamemterType = ParameterType<typeof describePerson>

const alex: DescribePersonParamemterType = {
  name: 'Alex',
  age: 20,
  hobbies: ['walking', 'cooking'],
}

describePerson(alex)

Object Literal And Structural Type System

這個不是我發現的,而是社群中看到的討論,做個筆記紀錄:TS Playground

function morningCheck(breakfast: { name: string}) {
  console.log(breakfast);
}

function noonCheck(lunch: { name: string, price: number }) {
  morningCheck(lunch);
}

const order = { name: "frittata", price: 250 }

// error - Object literal may only specify known properties
morningCheck({ name: "frittata", price: 250 });

morningCheck(order);
morningCheck({ name: "frittata", price: 250 } as {name: string, price: number});

把這個錯誤描述拿去 Google 可以看到 Stackoverflow 這則回答提到:

As of TypeScript 1.6, properties in object literals that do not have a corresponding property in the type they’re being assigned to are flagged as errors.

可以再對照一下 TypeScript 1.6 的更新說明

那麼關於 morningCheck(lunch) 這個用法不會報錯,可以參考:

The shape-matching only requires a subset of the object’s fields to match.

Structural Type System | TypeScript Documentation

上面這段節錄自官網,也就是以變數型式當參數時,只要符合該參數的形狀 (必須元素),多出來的屬性不會被視為錯誤。

社群中看到不錯的解釋脈絡是;如果用 object literal 的型式當參數,很可能就是針對這個函式而存在,就會嚴格檢查物件中的屬性,但如果以變數型式去傳參數,可能是其它地方也會用到,保留彈性的情況下,不做嚴格檢查,畢竟多出來沒用到的參數,理論上不會造成錯誤。有點像 像 Inferring Within Conditional Types 這段裡也有提到的概念。