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 BA が 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 では、ジェネリクスパラメータにはデフォルトの制約が存在します(以下の関数のジェネリクス、クラスのジェネリクスにも同様です)。このデフォルトの制約値は、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 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

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。