iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0

https://ithelp.ithome.com.tw/upload/images/20250831/20118113BLkCodRJyL.png

泛型

型別參數

如果型別也能成為參數,那麼型別的處理可以有更多的彈性和變化。接下來我們將介紹Typescript的generic型別可以適當的支援這個想法。當我們的函式需要處理多種型別的資料時,就可以使用generic型別來處理,例如:identity函式,它接受一個參數並返回該參數。我們可以讓這個函式適用於任何型別,而不需要為每種型別都寫一個函式。如果沒有使用泛型,我們可能須要替各種型別寫一個identity函式,例如:

const identity = (arg: number): number => arg;
const identity = (arg: string): string => arg;
const identity = (arg: boolean): boolean => arg;

這樣的寫法會讓程式碼變得冗長且重複,而且如果我們要新增一種新的型別,我們還需要再寫一個函式。
當然,我們也可以使用any型別來處理,但是這樣會失去型別檢查的好處。

const identity = (arg: any): any => arg;

使用泛型,我們可以這樣寫:

const identity = <T>(arg: T): T => arg;

在這邊T是一個型別變數,它代表任何型別。我們可以在函式名稱後面加上來表示這個函式是一個泛型函式。在函式體中,我們可以使用T來表示任何型別。這樣我們就可以讓這個函式適用於任何型別,而不需要為每種型別都寫一個函式。執行函式時,我們可以這樣寫:

let output = identity<string>("myString");

如此便可以輕鬆完成此類的函式多載。

再舉一個利用泛型來實作一個物件容器的建構函數of如下

interface Container<T> {
  _tag: 'Container';
  value: T;
}

const of = <T>(value: T): Container<T> => ({
    _tag: 'Container',
    value: value,
})

泛型也可以同時接受二個以上的泛型變數,下面用二個泛型變數實作一個簡單的map函數,這個函數可以接受一個函數和一個陣列,然後將函數應用於陣列中的每個元素,最後返回一個新的陣列。

const map = <T, U>(fn: (item: T) => U, arr: T[]): U[] {
  const newArray: U[] = [];
  for (const item of arr) {
    newArray.push(fn(item));
  }
  return newArray;
}

泛型約束

泛型可以限制型別的範圍,例如我們可以限制泛型變數的型別必須是某個型別的子型別,或者必須實現某個介面。這樣可以讓我們在編譯時期就發現型別錯誤,而不是在執行時期才發現。

interface Person {
    name: string;
    age: number;
    sayHello(): void;
}
const greet = <T extends Person>(person: T): void => {
    person.sayHello();
}

型別設計

型別的函數-Utility Types

我們也可以型別當作函數的參數,然後輸出另一個型別,這種函數稱為Utility Types,簡單的Utility Types可以像以下這樣:

type OrNull<T> = T | null;
type OneOrMany<T> = T | T[];

絛件式

由於集合的運算只有包含於,所以typescript中唯一的條件式使用便是下面的三元運算子配合extends關鍵字

type ResultType = TypeA extends TypeB ? ResultTypeC : ResultTypeD

上面的語法的意思是如果TypeA是TypeB的子集,那麼ResultType = ResultTypeC,否則ResultType = ResultTypeD。

雖然typescript沒有等於的符號,也沒有值的概念,但是我們可以利用單元素集合配合extends三元運算做到類似等於條件式。

type Event = 'click' | 'keyup' | 'change'

type EventAction<A extends Event> = A extends 'click' ? string : () => string

變數

Typescript提供infer的語法,讓我們推論某些型別並賦予型別一個變數名。infer關鍵字只能放於extens之後,?號之前,將想要推論的型別代入infer 型別變數名即可(通常是代入原本為any或unknown的地方),如此我們便可在三元運算元的?之後使用這個型別變數名。下面是一些常見推論情形:

  • 推論陣列型別
type Item<T> = T extends (infer U)[] ? U : never;
  • 推論物件型別
type Config<T> = T extends { config: infer C; } ? C : never;

推斷 Function 參數和回傳的型別

type Parameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;

type ReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;

物件型別Mapped types

Index signature type不能用在字面值型別(literal type)聯集(Union types),下面的寫法是不合typescript的規範。

type Name = 'name' | 'fistname' | 'lastname';
type Person = {
  [key :Name]: string;
}

因此當物件屬性的型別如果相同,我們要使用Mapped types,也就是在index accessed types中使用關鍵字in搭配Union types來重複定義某些屬性的型別,例如:

type Name = 'name' | 'fistname' | 'lastname';
type Person = {
  [key in Name]: string;
}

利用mapped types我們可以像迴圈一樣,輕鬆的將一個物件的屬性轉換成另一個物件的屬性,例如:

type UserEvent = {
  click: string;
  change: string;
  keyup: string;
  keydown: string;
};
type EventHandler = {
  [key in keyof UserEvent]: (event: UserEvent[key]) => void;
}

再搭配Template Literal Types和關鍵as,我們可以輕鬆的將一個物件的屬性名稱轉換成另一個物件的屬性名稱,例如:

type UserEvent = {
  click: string;
  change: string;
  keyup: string;
  keydown: string;
};

type EventHandler = {
  [key in keyof UserEvent as `handle${key}`]: (event: UserEvent[key]) => void;
}

上面程式碼會將UserEvent物件中所有的key都跑一遍,關鍵字as會將key的值轉換成handle${key}的格式。所以我們會得到一個新的物件EventHandler,這個物件的key會是handleclick、handlechange、handlekeyup、handlekeydown,而value會是(event: string) => void。

上面程式碼中可以發現,在indexed access types中的key這個變數名稱可以隨意命名,而且:後面的型別中也可以引用key這個變數,例如上面程式碼中的(event: UserEvent[key]) => void。

另外,配合物件型別的屬性修飾符readonly和optional,我們可以輕鬆的實現Partial、Required和Readonly型別utility types,

type Partial<T> = {
  [P in keyof T]?: T[P];
  };

type Required<T> = {
  [P in keyof T]-?: T[P];
};

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

有了三元運算符的conditional type和類似Local型別變數名的infer type讓我們可以做程式語言中的if-else的功能,而有了Mapped types則可以有類似迴圈的功能,我們可以進行型別Utility type的設計。

今日小結

型別的utility type其實可以視為型別的函數,舉例來說

Partial = <T> -> Partial<T>

Partial可以視為函數,T是我們的型別參數,也就是我們型別輸入,而輸出結果變是Partial<T>,而型別Utility type的設計也就是型別函數的設計。如果有興趣鑽研型別函數的設計可以參考一些utility types 函式庫,像是type-fest,ts-toolbelt,utility-types,…等型別函數庫。未來在學習fp-ts的時候,我們將會用utility type來建構型別,也稱這些型別函數為型別建構子,屆時再做更深入的探討,今日的分享就到這邊告一段落,明天再見。


上一篇
Day 03. 一切都是函數 - Function
下一篇
Day 05. 模組化設計 - 匯出(export) & 匯入(import)
系列文
數學老師學函數式程式設計 - 以fp-ts啟航5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言