[TS] Generics(泛型)
keywords: type argument
,type parameter
此篇為各筆記之整理,非原創內容,資料來源可見下方連結與文後參考資料。
- Creating Types from Types TypeScript > Handbook > Type Manipulation
- Generics @ TypeScript > Handbook > Type Manipulation
- TypeScript Fundamental v2 @ FrontEnd Master
- What is the syntax for Typescript arrow functions with generics? @ StackOver Flow
TL;DR
- 使用泛型的時候,要留意使用在
function
和一般的type
、interface
時會稍有不同。函式多了 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);