[TypeScript] Generics 的使用情境

不預先指定具體型別,只有在使用時才指定型別

Hannah Lin
Hannah Lin
14 min readJan 3, 2024

--

TypeScript 系列文

1. The Very Basics for TS
2. Generics 的使用情境
3. The `extends` keyword
4
. GenericsMapped Types
5. 用生活例子圖解 Utility Types
6
. 優雅的在 React 中使用 TS
7. 用 ts-migrate 仙女棒讓 JS 專案瞬間 migrate 成 TS

🔖 文章索引

1. What's Generics
2. When to use Generics

Generics 應該是 TS 中最讓人卻步的 part,看不懂的 TKV ,搭配 extends 、 Utility Type 如 Partial<T>Pick<T, K> 真的讓人一頭霧水。

// 😰😰😰
const getValue = <T, K extends keyof T>(obj: T, key: K) => {
return obj[key];
};

What’s Generics

Generics allow us to define placeholder types first, then replaced when the code is executed with the actual types passed in. Generics makes it easier to create reusable code to handle different inputs.

泛型(Generics)是一種在定義函式、介面或類別時,不先確定具體的型別,而是在實際使用時,再指定型別的特性。它的概念類似 JS 中的變數,可以使定義的 Type 更彈性並可重覆利用

學習 Generics 的第一步常會卡關,因為這個字讓人感覺非常抽象,好像不是一個東西而是代表多個東西 ?

// 傳了一個 Generic 給 function hello?
function hello<T>(data:T){
console.log(data)
}

hello<string>("Hannah") //data 參數型別會被代入為 string
hello<string>(111) //error 型別錯誤

// 傳了一個 Generic 給 useState?
useState<string>();

// 傳了兩個 Generics 給 Record?
type NumberRecord = Record<string, number>;

// 定義一個名叫 Maybe 的 generic?
type Maybe<T> = T | null | undefined;

Generics 會出現在 functions、 function calls、types、type declarations,比較像一個形容詞而不是代表某樣東西的名詞

Generic’ is an adjective

所以請就不要管 Generic 了,取而代之要知道的是以下五個 Type arguments、Type parameters、Generic types、Generic functions、Generic classes

// function hello 有一個名為 `T` 的 type Parameter,可代進任意輸入的型別。
function hello<T>(data:T){
console.log(data)
}

// 傳了一個 type argument(string) 給 useState 😀
useState<string>();

// 傳了兩個 type arguments(tring, number) 給 Record 😀
type NumberRecord = Record<string, number>;

// 定義了一個叫 Maybe 的 generic type 😀
type Maybe<T> = T | null | undefined;

Type arguments

使用時實際傳進去的型別

// string 就是 type argument
Maybe<string>

// { a: 1, b: 2} 就是傳進 T 裡的 type argument
const addIdObj = <T>(obj: T) => ({
...obj,
id: "123"
})
const result = addIdObj({
a: 1,
b: 2
})

Type parameters

定義時 (使用前)還不確定具體型別的參數

// T 就是 Type parameters
type Maybe<T> = xxx

Generic types

當 type 他宣告了一個 type parameter 時就是 Generic type

// Maybe 就是 Generic types,因為他宣告了一個 type parameters
type Maybe<T> = xxx

T 在賦值前可以為任何型別,只有在使用時才會明確指定型別參數

// Identity<T> 裡面的 T 在賦值前可以為任何型別
// 賦值後的 T 就會成為該型別
type Identity<T> = T;

let numberValue: Identity<number> = 1;
// error
let numberValue2: Identity<number> = "1";
let stringValue: Identity<string> = "Hello, world!";
let booleanValue: Identity<boolean> = true;
let arrayValue: Identity<number[]> = [1, 2, 3, 4, 5];

Generic functions

function 有 type parameter 就是 Generic functions

// Generic functions
function hello<T>(data:T){
console.log(data)
}

// non Generic functions
function hello(data){
console.log(data)
}

T (Type parameter) 就是 type 的變數,可以為任何型別。當然你不一定要用T ,任何字母都可以,只是大家習慣用 TK 而已 ( 在 JS 裡也有類似情況,例如大家喜歡用 i 代表 index ; 使用 x 在 for loop 裡)

hello<string>('Hi');
hello<number>(123);
hello<boolean>(true);

// 甚至使用型別推論簡化寫法, TS 會聰明幫你把剩下事做好
hello('Hi');
hello(123);
hello(true);
Generics 執行步驟,把型別傳進 <T>,再陸續傳到裡面

這樣的好處是很彈性去 re-use 它,因為若是使用上一篇的整理過的靜態 type,你就只能傳一開始就被定義好的型別

但若傳進來型別不確定 (這邊可能是 string ,另一邊可能是 number),就需要定義兩次重覆性很高的 function,這時就很適合使用 Generics Types 了。

Multiple type arguments

當然也是可以有多個 arguments 的

// Have 2 type arguments T、U
function callFun<T, U>(e1: T, e2: U): [U, T] {
return [e2, e1];
}

// 推薦:可以使用型別推論寫法
const a = callFun("world", 123);
console.log(a); // [123, "world"]
// 也可以使用型別註釋
const b = callFun<string,number>("world", 123);
console.log(b); // [123, "world"]

這邊來做一個小結論

  • 使用 function type: 函示參數型別是固定的
  • 使用 Generic functions: 傳進來的參數型別不固定

When to use Generics

Generics 被應用到各式各樣情境,若你遇到以下情境,就是 Generics 上場的最佳時機

  • 不確定傳進來的是什麼型別
  • multiple different type (在不同地方使用會是不同 type): 例如這邊是 number,另一個邊 string

大體來說可以分為以下三種 Patterns

// 1. Passing Type to Type
type MyGenericType<T> = {
data: T
}
type example = MyGenericType<{name: string}>

// 2. Passing types to functions
const stringSet = new Set<string>();

// 3. Inferring the types from arguments passed to functions
const addIdObj = <T>(obj: T) => ({
...obj,
id: "123"
})
const result = addIdObj({
a: 1,
b: 2
})
// ^? const result: { a: number; b: number; } & { id: string; }

Generics Type: Passing Type to Type

在 TS 你先定義好 Type,而這個 type 可以是物件、function、原始型別 primitive 等。直接舉一個實務例子:

若我們有一個 ApiResponse 的 type,並不確定 data 會傳進什麼,那就會在 data 那邊定義 any type。

type ApiResponse = {
data: any
isError: boolean
}

const reponse: ApiResponse = {
data: {
name: 'Hannah',
age: 26
},
isError: false
}

但其實有更好寫法

type ApiResponse<Data> = {
data: Data
isError: boolean
}

type UserResponse = ApiResponse<{ name: string, age: number }>
const reponse: UserResponse = {
data: {
name: 'Hannah',
age: 26
},
isError: false
}

這樣的話就可 一直 reuse ApiResponse 這個 type

type UserResponse = ApiResponse<{ name: string, age: number }>
type BlogResponse = ApiResponse<{ title: string }>
type StatusResponse = ApiResponse<{ status: number }>

const reponse: UserResponse = {
data: {
name: 'Hannah',
age: 26
},
isError: false
}
const reponseBlog: BlogResponse = {
data: {
title: 'I am a title'
},
isError: false
}

甚至可以進一步設 type 的 default value,若啥都沒傳至少會有 default type,並且規定 Data 一定要是 object (Data extends object)

type ApiResponse<Data extends object = {status: number}> = {
data: Data
isError: boolean
}

type UserResponse = ApiResponse<{ name: string, age: number }>
type BlogResponse = ApiResponse<{ title: string }>
type StatusResponse = ApiResponse
const reponseStatus: StatusResponse = {
data: {
status: 200
},
isError: false
}

Generics Function: Passing types to functions

create function with type helper over the top

// Native function
const set = new Set<number>()
useState<string | null>(null)

// Custom function
getFirstEle<string>

常用在同一個參數但可能有不同的型別的時候

function getFirstEle<T>(arr: T[]) {
return arr[0]
}

const num = [1, 2, 3]
const firstNum = getFirstEle<number>(num)
// ^? const firstNum = (arr: number[]) => {...}
const strs = ['apple', 'banana', 'orange']
const firstStr = getFirstEle<string>(strs)
// ^? const f firstStr = (arr: string[]) => {...}

你可能想說其實我不需要用到 Generics,直接 arr: (number|string)[] 就好啦,但這樣就算 firstNum 傳入字串也不會爆錯誤

function getFirstEle(arr: (number|string)[]) {
return arr[0]
}
// tricky here, getFirstEle here should only accept number
const firstNum = getFirstEle(['apple', 'banana', 'orange'])

另外在用原生 DOM 抓值時也很常需要 pass type 給 Document method

const input = document.querySelector('.input')
input?.value // error here, because by default Element don't have value property

const input = document.querySelector<HTMLInputElement>('.input')
// correct

Inferring the types from arguments passed to functions

you don’t always have to pass the types to a generic 你不需要每次都手動定義參數的型別,有些狀況可以讓 TS 自動幫你生


// 自動查找參數 type 帶入
const addIdObj = <T>(obj: T) => ({
...obj,
id: "123"
})
const result = addIdObj({
firstName: "Hannah",
lastName: "Lin"
})

Generics 還可以延伸出各式各樣的好用 Utility Type 會在之後文章介紹。

--

--