類型別名中的泛型#
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface IFoo {
prop1: string;
prop2: number;
prop3: boolean;
prop4: () => void;
}
type PartialIFoo = Partial<IFoo>;
// 等價於
interface PartialIFoo {
prop1?: string;
prop2?: number;
prop3?: boolean;
prop4?: () => void;
}
類型別名與泛型的結合中,除了映射類型、索引類型等類型工具以外,還有一個非常重要的工具:條件類型
type IsEqual<T> = T extends true ? 1 : 2;
type A = IsEqual<true>; // 1
type B = IsEqual<false>; // 2
type C = IsEqual<'linbudu'>; // 2
泛型約束與預設值#
type Factory<T = boolean> = T | number | string;
除了聲明預設值以外,泛型還能做到一樣函數參數做不到的事:泛型約束。也就是說,你可以要求傳入這個工具類型的泛型必須符合某些條件,否則你就拒絕進行後面的邏輯。在泛型中,我們可以使用 extends
關鍵字來約束傳入的泛型參數必須符合要求。關於 extends,A extends B
意味著 A 是 B 的子類型,這裡我們暫時只需要了解非常簡單的判斷邏輯,也就是說 A 比 B 的類型更精確,或者說更複雜。具體來說,可以分為以下幾類。
- 更精確,如字面量類型是對應原始類型的子類型,即
'linbudu' extends string
,599 extends number
成立。類似的,聯合類型子集均為聯合類型的子類型,即1
、1 | 2
是1 | 2 | 3 | 4
的子類型。 - 更複雜,如
{ name: string }
是{}
的子類型,因為在{}
的基礎上增加了額外的類型,基類與派生類(父類與子類)同理。
type ResStatus<ResCode extends number> = ResCode extends 10000 | 10001 | 10002
? 'success'
: 'failure';
type Res1 = ResStatus<10000>; // "success"
type Res2 = ResStatus<20000>; // "failure"
type Res3 = ResStatus<'10000'>; // 類型“string”不滿足約束“number”。
在 TypeScript 中,泛型參數存在預設約束(在下面的函數泛型、Class 泛型中也是)。這個預設約束值在 TS 3.9 版本以前是 any,而在 3.9 版本以後則為 unknown。在 TypeScript ESLint 中,你可以使用 no-unnecessary-type-constraint 規則,來避免代碼中聲明了與預設約束相同的泛型約束。
多泛型關聯#
type Conditional<Type, Condition, TruthyResult, FalsyResult> =
Type extends Condition ? TruthyResult : FalsyResult;
// "passed!"
type Result1 = Conditional<'linbudu', string, 'passed!', 'rejected!'>;
// "rejected!"
type Result2 = Conditional<'linbudu', boolean, 'passed!', 'rejected!'>;
多泛型參數其實就像接受更多參數的函數,其內部的運行邏輯(類型操作)會更加抽象,表現在參數(泛型參數)需要進行的邏輯運算(類型操作)會更加複雜。
上面我們說,多個泛型參數之間的依賴,其實指的即是在後續泛型參數中,使用前面的泛型參數作為約束或預設值:
type ProcessInput<
Input,
SecondInput extends Input = Input,
ThirdInput extends Input = SecondInput
> = number;
對象類型中的泛型#
interface IRes<TData = unknown> {
code: number;
error?: string;
data: TData;
}
interface IUserProfileRes {
name: string;
homepage: string;
avatar: string;
}
function fetchUserProfile(): Promise<IRes<IUserProfileRes>> {}
type StatusSucceed = boolean;
function handleOperation(): Promise<IRes<StatusSucceed>> {}
interface IPaginationRes<TItem = unknown> {
data: TItem[];
page: number;
totalCount: number;
hasNextPage: boolean;
}
function fetchUserProfileList(): Promise<IRes<IPaginationRes<IUserProfileRes>>> {}
函數中的泛型#
假設我們有這麼一個函數,它可以接受多個類型的參數並進行對應處理,比如:
- 對於字符串,返回部分截取;
- 對於數字,返回它的 n 倍;
- 對於對象,修改它的屬性並返回。
function handle<T>(input: T): T {}
const handle = <T>(input: T): T => {}
const handle = <T extends any>(input: T): T => {}
const author = "linbudu"; // 使用 const 聲明,被推導為 "linbudu"
let authorAge = 18; // 使用 let 聲明,被推導為 number
handle(author); // 填充為字面量類型 "linbudu"
handle(authorAge); // 填充為基礎類型 number
我們為函數聲明了一個泛型參數 T,並將參數的類型與返回值類型指向這個泛型參數。這樣,在這個函數接收到參數時,T 會自動地被填充為這個參數的類型。這也就意味著你不再需要預先確定參數的可能類型了,而在返回值與參數類型關聯的情況下,也可以通過泛型參數來進行運算。
在基於參數類型進行填充泛型時,其類型信息會被推斷到盡可能精確的程度,如這裡會推導到字面量類型而不是基礎類型。這是因為在直接傳入一個值時,這個值是無法再被修改的,因此可以推導到最精確的程度。而如果你使用一個變量作為參數,那麼只會使用這個變量標註的類型(在沒有標註時,會使用推導出的類型)。
function swap<T, U>([start, end]: [T, U]): [U, T] {
return [end, start];
}
const swapped1 = swap(["linbudu", 599]);
const swapped2 = swap([null, 599]);
const swapped3 = swap([{ name: "linbudu" }, {}]);
函數的泛型參數也會被內部的邏輯消費,如:
function handle<T>(payload: T): Promise<[T]> {
return new Promise<[T]>((res, rej) => {
res([payload]);
});
}
Class 中的泛型#
class Queue<TElementType> {
private _list: TElementType[];
constructor(initial: TElementType[]) {
this._list = initial;
}
// 入隊一個隊列泛型子類型的元素
enqueue<TType extends TElementType>(ele: TType): TElementType[] {
this._list.push(ele);
return this._list;
}
// 入隊一個任意類型元素(無需為隊列泛型子類型)
enqueueWithUnknownType<TType>(element: TType): (TElementType | TType)[] {
return [...this._list, element];
}
// 出隊
dequeue(): TElementType[] {
this._list.shift();
return this._list;
}
}
內置方法中的泛型#
function p() {
return new Promise<boolean>((resolve, reject) => {
resolve(true);
});
}
const arr: Array<number> = [1, 2, 3];
// 類型“string”的參數不能賦給類型“number”的參數。
arr.push('linbudu');
// 類型“string”的參數不能賦給類型“number”的參數。
arr.includes('linbudu');
// number | undefined
arr.find(() => false);
// 第一種 reduce
arr.reduce((prev, curr, idx, arr) => {
return prev;
}, 1);
// 第二種 reduce
// 報錯:不能將 number 類型的值賦值給 never 類型
arr.reduce((prev, curr, idx, arr) => {
return [...prev, curr]
}, []);
reduce 方法是相對特殊的,它的類型聲明存在幾種不同的重載:
- 當你不傳入初始值時,泛型參數會從數組的元素類型中進行填充。
- 當你傳入初始值時,如果初始值的類型與數組元素類型一致,則使用數組的元素類型進行填充。即這裡第一個 reduce 調用。
- 當你傳入一個數組類型的初始值,比如這裡的第二個 reduce 調用,reduce 的泛型參數會默認從這個初始值推導出的類型進行填充,如這裡是
never[]
。
其中第三種情況也就意味著信息不足,無法推導出正確的類型,我們可以手動傳入泛型參數來解決:
arr.reduce<number[]>((prev, curr, idx, arr) => {
return prev;
}, []);
References:
https://juejin.cn/book/7086408430491172901