LoginSignup
0
0

【TypeScript】ジェネリクス、lookup型、keyof型、部分型がまとめて理解できるコード例

Last updated at Posted at 2024-04-13

目標

以下のコードが理解できるようになることが本記事の目標です!

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

type Human = {
	name: string;
	age: number;
}

const tanaka: Human = {
  name: "tanaka",
  age: 26
}

const tanakaName = get(tanaka, "name"); // tanakaNameはstring型
console.log(tanakaName); // 出力: "tanaka"

const tanakaAge = get(tanaka, "age"); // tanakaAgeはnumber型
console.log(tanakaAge); // 出力: 26

動作解説

定義されているget関数がやっていることは、引数として渡されたobj[key]を返すだけです。

tanakaオブジェクトのnameプロパティにアクセスすれば、”tanaka”が取得できますし、ageプロパティにアクセスすれば26が取得できます。

型を省いてJavaScriptで実装すると以下のようになります。とてもシンプルですね。

function get(obj, key) {
  return obj[key];
}

const tanaka = {
  name: "tanaka",
  age: 26
}

const tanakaName = get(tanaka, "name");
const tanakaAge = get(tanaka, "age");

ただ、これをTypeScriptで実装しようとする時、いくつか問題点があります。

  1. get関数の引数に決まった型がない(第1引数のオブジェクトはHuman型であるとは限らない)
  2. 返り値の型がstringnumberで異なる
  3. get関数の第2引数はtanakaのプロパティだけを指定できるようにしたい

ジェネリクス

ジェネリクスを使うと、型は関数を呼び出した時に決める、ということはできるようになります。これを使えば1つ目の問題が解決しますね。

簡単な例を見てみましょう。

function test<T>(arg: T): T {
	return arg;
}

test<number>(1); // number型の値、1が返ってくる
test<string>("文字列"); // string型の値、"文字列"が返ってくる

抽象的な型引数Tを関数に与え、実際に利用されるまで型が確定しない関数を作成することによって、後で型を決められるようにしています。抽象的な型引数は複数渡すことができます。

ここで最初のコードを見てみましょう。

// 一部簡略化
function get<T, K>(obj: T, key: K): T[K] {
  return obj[key];
}

get関数にはTKという抽象的な型引数が渡されています。TKget関数呼び出し時に与えることにしている、ということです。objの型は後から渡されるTが、keyにはKが型として渡されます。

ですが実際にget関数を呼び出している部分を見てみると、TKに該当する方が渡されていません。このような時は型推論によってTKは決められます。get(tanaka, "name");とした場合、実際には以下のような型に推論されます。

function get<Human, "name">(obj: Human, key: "name"): string

THuman型、K”name”というリテラル型に推論されます。返り値であるT[K]stringに推論されていますが、これを理解するにはlookup型を理解する必要があります。

lookup型

lookup型はT[K]という構文を持つ型で、TKは型です。Tにはオブジェクト型、Kには文字列のリテラル型が用いられることが多いです。簡潔に書くと「T[K]Tが持つKプロパティの型」ということです。

今回解読しようとしているコードでは、THumanというオブジェクト型、K”name”という文字列のリテラル型が割り当てられています。Human型を見てみると、nameプロパティの型はstringになっています。よってT[K]stringに推論されているというわけです。

type Human = {
	name: string; // Human[name]のとき、T[K]はstring
	age: number; // Human[age]のとき、T[K]はnumber
}

lookup型を使うことによって、get関数の返り値の型が異なるという、2つ目の問題が解決されます。

keyof型

最後に3つ目の問題についてですが、これは型引数KにくっついているK extends keyof Tという部分が解決してくれます。この構文はextendskeyofという2種類の構文が混じっているため、一つずつ見ていきましょう。

まずはkeyofについてです。keyof型は、オブジェクト型からそのオブジェクトのプロパティ名の型を得る機能です。

type Test = {
	id: number;
	pass: string;
}
// "id" | "pass" という型と同義
type TestKeys = keyof Test;

let key: TestKeys = "id";
key = "pass";
key = "hoge" // これはエラー

Testkeys型は、Test型のプロパティ名だけが許されている型です。”id””pass”というリテラル型しか受け付けません。keyof型を使うとこのような型を簡単に作ることができます。

今回解読しようとしているコードでは、型引数TにはHuman型が割り当てられます。つまりkeyof T”name” | “age”ということになります。

部分型

続いてextendsについてです。これは部分型という制約があることを表しています。部分型は2つの型の互換性を表すものです。

type Animal = {
	age: number;
}

type Human = {
	name: string;
	age: number;
}

Animal型とHuman型がこのように定義されているとき、Human型はAnimal型の部分型であると言えます。Animalを満たすオブジェクトがHumanを満たすとは限りませんが、Humanを満たすオブジェクトはAnimalを必ず満たします。同じageプロパティをもち、その型も同じであるからです。このような関係にある型を部分型関係だと言えます。

部分型について理解いただけたところで、extendsについて解説します。extendsは、型引数に対して何らかの部分型であるという制約をつけたい時に使われます。

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

get関数の場合、型引数Ktypeof Tの部分型でなければならないという制約が課されています。つまりK”name” | “age”の部分型でなければならないということです。この制約があることによって、get関数の第2引数はHuman型のプロパティ名のリテラル型しか受け付けないようになっています。

おまけ:Kにextends keyof Tを指定しないと…

ジェネリクスの解説の時にget関数の型は以下のように推論されると書きましたが、keystringに推論されるのではないか?と思った方がいるかも知れません。

function get<Human, "name">(obj: Human, key: "name"): string

型引数 Kに対してextends keyof Tという制約がなければ、keystringに推論されます。しかしKに対してextends keyof Tを指定しない場合、「Type ‘K’ cannot be used to index type ‘T’」というコンパイルエラーになります。get関数の返り値の型T[K]について、不都合が生じる可能性があるからです。T[K]の意味からすると、Tは何らかのオブジェクト型で、Kはそのオブジェクト型が持つプロパティである必要があります。extends keyof Tがないとこれが保証できなくなるため、コンパイルエラーとなるのです。

まとめ

今回はTypeScriptに欠かせないジェネリクスや部分型にプラスして、lookup型やkeyof型についての解説も行いました。後者の2つを使いこなすには練習が必要ですね。

コード解説の部分で提示したJavaScriptのコードを、コンパイルエラーが出ないように型を付け足していくと、今回の内容が復習できるのではないかと思います。ぜひ挑戦してみてください。

本記事に誤りや誤字脱字がございましたら、コメント等でご指摘いただけるとありがたいです。

参考

ジェネリクス (generics) | TypeScript入門『サバイバルTypeScript』

【TypeScript】Generics(ジェネリックス)を理解する - Qiita

TypeScript の lookup 型をマスターしよう - コムテブログ

keyof型演算子 | TypeScript入門『サバイバルTypeScript』

部分型って"拡張型"じゃない?集合論で捉えてみる

【Typescript入門】ややこしい?部分型について初心者向けに紹介! - プロブロ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0