⚡️

<T, K extends keyof T> をちゃんと理解する

2024/06/20に公開

TL;DR

TypeScriptでライブラリなどを覗いているとよく見かける<T, K extends keyof T>について、問題を分解してきちんと理解する試みです。

extends

この場合のextendsの役割は継承ではなく、制約です。

例えば以下のようなイメージ。

type StringOrNumber<T extends string | number> = T
type A = StringOrNumber<string>
type B = StringOrNumber<number> 

// Type 'boolean' does not satisfy the constraint 'string | number'
type C = StringOrNumber<boolean>

エラーメッセージの指摘がまさにそうですよね。Tthe constraint 'string | number'です。

次はオブジェクト型の場合を見てみます。

type User = {
  id: number;
  name: string;
};

const getUserName = <T extends User>(arg: T): string => arg.name;

getUserName({ id: 123, name: 'aaa' });
getUserName({ id: 123, name: 'aaa', email: 'aaaaaa' });

// Argument of type '{ id: number; }' is not assignable to parameter of type 'User'.
// Property 'name' is missing in type '{ id: number; }' but required in type 'User'
getUserName({ id: 123 });

こちらも同じくtype 'User'では'name'が必須ですという旨のことを言われています。

つまり、K extends Tとは少なくとも、KはTのプロパティ・型を持ってないとダメということになります。

keyof

こちらは比較的簡単な話で、keyofはオブジェクトの型からプロパティ名を型として返す型演算子です。

type User = {
  id: number;
  name: string;
};
// このコードはどちらも同じ意味
type UserKey = keyof User; // "id" | "name"

const id: UserKey = "id"
const useName: UserKey = "name"

// Type '"age"' is not assignable to type 'keyof User'.
const age: UserKey = "age"

<T, K extends keyof T> を理解する

例えばTが先ほどのUserだったとします。そのとき、keyof User"id" | "name"となります。

かつ、extendsをふまえるとK extends "id" | "name"ということになります。

つまりK"id","name", "id" | "name"のいずれかであれば取ることができますが、"age""id" | "age"をとることはできません。

で、なにが便利なの?

それを知るにはtype-challengesのこの問題を解いてみましょう。

この問題を解くためには、extends, keyof, Mapped Typesの3つの知識を組み合わせる必要があります。

組み込みの型ユーティリティPick<T, K>を使用せず、TからKのプロパティを抽出する型を実装します。

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
}

ここで、MyPickをこのように定義することができます。

type MyPick<T, K extends keyof T> = {
  [key in K]: T[key]
}

KTのいずれかのプロパティとなり、そのプロパティをkeyとして取り出します。

その上で、T[key]よりプロパティkeyに対応する型をセットします。

結果として、'title' | 'completed'のようにPickしたいプロパティを指定すれば、Pickと同等の結果を得られるというわけです。

おわりに

type-challengesの初級のしょっぱなの問題がこれだったのですが、正直「むずすぎん…?」て感じでした。

いくつかの基本を組み合わせて解かないとダメなので、これ完全に応用じゃん…てなりました。

ただしっかり言語化していくと、とても良い勉強にはなったので良問でした👍

📢 Kobe.tsというTypeScriptコミュニティを主催しています

フロント・バックエンドに限らず、周辺知識も含めてTypeScriptの勉強会を主催しています。

毎朝オフラインでもくもくしたり、神戸を中心に関西でLTもしています。

盛り上がってる感を出していきたいので、良ければメンバーにだけでもなってください😣

https://kobets.connpass.com/

Discussion