Kodokon kodokon.com

高度なジェネリクス: 制約とデフォルト

extends で型パラメータを制約し、デフォルトを提供して、扱いやすいジェネリック API を構築します。

10 分 · 3 問

このレッスンを Kodokon で開く

制約のないジェネリックは不透明です。関数本体の中では、T に対して何もできません。extends 句は型パラメータに最小限の契約を課し、これによって保証されたプロパティへのアクセスが可能になりつつ、呼び出し側の正確な型が保持されます。これがインターフェースで型付けされた引数との根本的な違いです。制約は情報を保持し、インターフェースはそれを消し去ります。

TYPESCRIPT
function longest<T extends { length: number }>(
  a: T,
  b: T
): T {
  return a.length >= b.length ? a : b;
}

const word = longest("hello", "worldwide");
const list = longest([1, 2], [3, 4, 5]);
word は string、list は number[] として型付けされたままです。

K extends keyof T の組み合わせは、日常で最も役立つパターンです。2 つの型パラメータを互いに結び付け、あるキーが実際にオブジェクトに属することを静的に保証します。返り値の型 T[K]インデックスアクセス型)は、要求されたプロパティを正確に追跡します。これはユーティリティライブラリの pickpluck のような関数がまさにこうして型付けされている方法です。

TYPESCRIPT
function getProp<T extends object, K extends keyof T>(
  obj: T,
  key: K
): T[K] {
  return obj[key];
}

const config = { port: 8080, host: "localhost" };
const port = getProp(config, "port");
const host = getProp(config, "host");
port は number、host は string と推論され、他のどんなキーも拒否されます。

デフォルトT = string)は、呼び出し側でジェネリックを省略可能にします。利用者は、よくあるケースから外れたいときにだけ型引数を渡せばよくなります。制約と組み合わせると(T extends X = Y)、上限と暗黙の振る舞いの両方を定義できます。デフォルトは制約を満たさなければならず、そうでないと宣言自体がコンパイルできません。これは HTTP クライアント、ストア、型付きイベントエミッターの標準的なパターンです。

TYPESCRIPT
interface ApiResponse<T = unknown> {
  status: number;
  data: T;
}

async function fetchJson<T = unknown>(
  url: string
): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  return { status: res.status, data: await res.json() };
}

interface Post { id: number; title: string }
const typed = await fetchJson<Post>("/api/posts/1");
const loose = await fetchJson("/api/health");
型引数がないと、data は any ではなく unknown にフォールバックします。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. ジェネリックなシグネチャにおいて、K extends keyof T という制約は何を保証しますか?
    • K が任意の文字列であること
    • K が T の既存のキーであり、コンパイル時にチェックされること
    • T が少なくとも 1 つの省略可能なキーを含むこと
    • K と T が同じ型であること
  2. ジェネリクスなしの longest(a: { length: number }, b: ...) に対して、longest<T extends { length: number }> にはどんな利点がありますか?
    • 実行が速い
    • 返り値において呼び出し側の正確な型を保持する
    • より多くの入力型を受け入れる
  3. デフォルト値として <T = any> よりも <T = unknown> を優先するのはなぜですか?
    • unknown は any よりコンパイルが速い
    • any は strict モードで禁止されている
    • unknown は使用前に呼び出し側に型を絞り込ませるが、any はすべてのチェックを無効にする
    • unknown は any より受け入れる値が少ない