跳至主要内容

[TS] Generics(泛型)

keywords: type argument,type parameter

此篇為各筆記之整理,非原創內容,資料來源可見下方連結與文後參考資料。

TL;DR

  • 使用泛型的時候,要留意使用在 function 和一般的 typeinterface 時會稍有不同。函式多了 type argument inference,TS 會根據「呼叫函式時」帶入參數的型別(或回傳值的型別),自動推導函式中泛型的型別
// type
type Dict<T> = {
value: T;
};

// interface
interface WrappedValue<T> {
value: T;
}

// arrow function
const identity = <T>(x: T): T => x;

// function
function identity<T>(x: T): T {
return x;
}

基本語法

Generics(泛型)很重要的一點,就是讓我們寫的方法可以適用在不同的型別,而非只能使用在單一型別。

// https://www.typescriptlang.org/docs/handbook/2/generics.html

// arrow function 能在 .ts 但不能在 .tsx 中使用
// 因爲 TS 會無法清楚區分 <> 指的是 JSX 或 Generics Type
const identity = <T>(x: T) => x;

// identity 這個 function 接收型別為 T 的參數,並回傳型別為 T 的參數
function identity<T>(x: T): T {
return x;
}
備註

因為在 .tsx 中,TS compiler 會無法清楚區分 <> 是指 JSX 或 Generics Type,雖然有些 work around 可以避掉,但最簡單的方式就是「當需要在函式中定義泛型時,就直接使用傳統的 Function Statement」

Type Argument Inference

一般來說,可以直接使用該函式,而 TS 會根據「呼叫函式時所『帶入參數的型別』、『回傳值的型別』」,自動推導 T 的型別,這個過程稱作 type argument inference

/* type argument inference */

// 雖然沒有告知 TS function identity<T>(x: T) 的 T 型別為何
// 但 TS 會自動根據帶入函式中參數 x 的型別來推導 T 的型別
// 因爲 x 的
const id = identity('foo');

若有需要的話,也可以在使用帶有泛型的 utility type 後,可以把該函式的型別帶入:

let id = identity<string>('foo');

定義泛型

// type
type Dict<T> = {
value: T;
};

// interface
interface WrappedValue<T> {
value: T;
}

// arrow function
const identity = <T>(x: T): T => x;

// function
function identity<T>(x: T): T {
return x;
}

帶有限制的泛型 Type Parameter with Constraints

透過 extends 的使用,可以建立帶有「限制」的泛型:

interface WrappedValue<T extends string> {
value: T;
}

// ⭕️ T 滿足 string 的型別
const val: WrappedValue<'Aaron' | 'PJ'> = {
value: 'Aaron',
};

// ❌ T 不滿足 string 時會噴錯
// Type 'number' does not satisfy the constraint 'string'.
const val: WrappedValue<number> = {
value: 30,
};

// ❌ 因為沒有給 T 預設值,所以不能留空
// Generic type 'WrappedValue<T>' requires 1 type argument(s).
const val: WrappedValue = {
value: 30,
};

實際的例子像這樣:

// 接收 array: T[] 做為參數,回傳 Dictionary {[k: string]: T}
// 若把 T 的限制 { id: number } 拿掉的話,將會沒辦法確定 T 是帶有 id 的物件
const arrayToDict = <T extends { id: number }>(array: T[]): { [k: string]: T } => {
const out: { [k: string]: T } = {};
array.forEach((val) => {
out[val.id] = val;
});
return out;
};
提示

要了解泛型中為什麼加上限制最快的方式,是把該限制拿掉,然後看 TS 有沒有出現錯誤提示。

帶有預設值的泛型 Generic parameter defaults

可以給泛型預設值,舉例來說,下面的程式碼指的是:

  • 沒有給 T 的話,T 預設的型別會是 string
interface WrappedValue<T = string> {
value: T;
}

但如果只是定義預設值的話,使用者可以任意更改該型別,例如:

// 預設 WrappedValue 中使用的型別是 string,但使用者可以改成自己想要使用的
const val: WrappedValue<number> = {
value: 30,
};

這時候,可以透過 extends 限制使用者給定的泛型,例如:

  • 如果沒有提供 T 則預設該型別是 string
  • 如果有給 T
    • T 需要滿足 string,否則會噴錯
    • T 滿足 string 時,T 會變成變成實際代表的型別
interface WrappedValue<T extends string = string> {
value: T;
}

// 如果沒有提供 `T` 則預設該型別是 `string`
const val: WrappedValue = {
value: 'hello',
};

// ❌ 如果有給 T,但 T 不滿足 string 時,會噴錯
// Type 'number' does not satisfy the constraint 'string'.
const val: WrappedValue<number> = {
value: 30,
};

// ⭕️ 如果有給 T,當 T 滿足 string 時,T 會變成變成實際代表的型別
// const val: WrappedValue<'Aaron' | 'PJ'> = {
value: 'Aaron',
};

Generics Constraint

在我們使用泛型時,它會被視為「任何(any)」和「所有型別(all types)」。此時。若我們這樣寫會報錯,因為編譯器沒辦法確定 T 是否有對應的 .length 方法:

// https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints
function identity<T>(arg: T): T {
console.log(arg.length); // Property 'length' does not exist on type 'T'
return arg;
}

為了要確保 T 一定帶有 .length 這個方法,我們可以透過定義 interface 搭配 T extends Interface來的方式來限制 T 能夠使用的方法:

interface ILengthwise {
length: number;
}

// T 一定要滿足 ILengthwise interface
function identity<T extends ILengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}

// 錯誤:Argument of type 'number' is not assignable to parameter of type 'ILengthwise'
// 因為 number 不能滿足有 length 這個 property
const id = identity(2);

// 正確:由於有滿足 ILengthwise interface 所以可以
const result = identity({ length: 30 });

// 箭頭函式的話可以寫成這樣
const loggingIdentity: <T extends ILengthwise>(arg: T) => T = (arg) => {
console.log(arg.length);
return arg;
};

Using Type Parameters in Generic Constraints

我們也可以根據另一個 Type 來限制另一個 Type。舉例來說,為了確保 T 這個型別裡有 K 這個屬性時,可以這樣寫:

// K 一定要滿足是 T 的 property
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}

// 直接讓 compiler 推斷型別
const value = getProperty({ foo: 'bar' }, 'foo');

// 或者將型別明確給入
interface IPayload {
foo: string;
}
const value = getProperty<IPayload, 'foo'>({ foo: 'bar' }, 'foo');

泛型也有 scope 的概念

/**
* 泛型一樣有 scope 的概念
*/
const startTuple =
<T>(a: T) =>
<U>(b: U) =>
[a, b] as [T, U];

// 上面 arrow function 和這個的意思相同
// function startTuple<T>(a: T) {
// return function finishTuple<U>(b: U) {
// return [a, b] as [T, U];
// };
// }

// const myTuple: [string[], number]
const myTuple = startTuple(['first'])(42);