Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

TypeScript 泛型

類型別名中的泛型#

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 string599 extends number 成立。類似的,聯合類型子集均為聯合類型的子類型,即 11 | 21 | 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

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。