All Articles

type TypeKey<T> = { [K in keyof T]: K }[keyof T] これ読めますか?

はじめに

TypeScript を勉強始めて一ヶ月ほど経ちますが、まだコードをしっかり書けるようになっていません。 引き続きコードを書きつつ Qiita やフロント界隈で有名でとても優秀な方の記事もコツコツ読んでいきたいです。

今回とある理由でライブラリを読んでいるのですが、

type TypeKey<T> = { [K in keyof T]: K }[keyof T]

この型宣言が出てきた時に、何のことかわかりませんでした。

こういうのをスラスラ読めるようになることで脱初心者に一歩近づけるのかな。

Google 先生に聞きながら自分なりに腹落ちしたので、数週間後または数ヶ月後に忘れてしまう自分のために文章で残していきます。

そういえば、Swift の Generics まわりがかなり怪しいので、TypeScript からの逆輸入にはなりますが、復習しながら備忘録を Blog にしたいな〜。

Mapped Types + Lookup Types

この文法を理解するには、

  • Mapped Types
  • Lookup Types

を理解する必要があるようなので、おさらいします。 Notes on TypeScript: Mapped Types and Lookup Types のページをメインに学習したので、こちらのページにかかれている例を転記していきます。

Mapped Types

{ [ P in K ] : T } の表記で型を動的に作り出すもの。

こちらは、オフィシャルのMapped types にわかりやすい例がありましたので転機します。

type Item = { a: string, b: number, c: boolean };

type T1 = { [P in "x" | "y"]: number };  // { x: number, y: number }

確かに型を作り出していますね。これはしっくり。

keyof を使った少し複雑な例

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

type MakeReadOnly<Type> = {readonly [key in keyof Type ]: Type[key]};
// Test MakeReadOnly
type ReadOnlyUser =  MakeReadOnly<User>;

/* Result
type ReadOnlyUser = {
  readonly id: number;
  readonly name: string;
  readonly points: number;
}
*/

[key in keyof Type ]:

keyof で型からプロパティ名(Key)を取り出して、in により順次処理 Type[key] 型からプロパティ型(number , string)を取り出す

Lookup Types

Lookup Types は以下の例のように、プロパティ名を列挙することで、プロパティの型を取り出せる

  • プロパティの型を取り出す
type UserKeyTypes = User["id" | "name" | "points"];

/*
type UserKeyTypes = number | string;
*/

手動で、一つずつプロパティ名を列挙しなくても、 keyof を使えばスッキリ書けます。

  • プロパティの型を取り出す(Keyof)
type UserKeyTypes = User[keyof User];

/*
type UserKeyTypes = number | string;
*/

keyof は至るところで使うのでしっかり覚えておきたいところ。

プロパティ名を取り出したい場合は、以下のように書く。 [] で囲まないだけの違いですね。

  • プロパティ名を列挙する。
type UserKey = keyof User

/*
type UserKey = "id" | "name" | "points"
*/

Mapped Types と Lookup Types を組み合わせる

  • Optional のプロパティを取り除いた、プロパティ名を列挙する
type RemoveUndefinable<Type> = {
  [Key in keyof Type]: undefined extends Type[Key] ? never : Key
}[keyof Type];

これは 2 つに分解して考えられます。

  • Mapped Types 型から Optional ではないプロパティを取り出す
type RemoveUndefinableKeys<Type> = {
  [Key in keyof Type]: undefined extends Type[Key] ? never : Key
};
  • keyof によりプロパティからプロパティ名を取り出す(Lookup Types)
type RemoveUndefinable<Type> = RemoveUndefinableKeys<Type>[keyof Type]

任意の条件のプロパティ名を取り出すには、以下のような構文になるのですね。

{ Mapped Types } [ keyof Type ]

{ Mapped Types } が新しい型を作っていると考えると、Lookup Types の以下の例が読めれば理解できますね。

type UserKeyTypes = User[keyof User];

これを使うと 必須プロパティのみの新しい型を作り出すことが可能となります。

type RemoveNullableProperties<Type> = {
  [Key in RemoveUndefinable<Type>]: Type[Key]
};

type TestRemoveNullableProperties = RemoveNullableProperties<{
  id: number;
  name: string;
  property?: string;
}>;

/* Result
type TestRemoveNullableProperties = {
  id: number;
  name: string;
};
*/

※ Visual Studio では上記のTestRemoveNullableProperties を引数に受ける関数にpropertyを渡してテストしたところ、Jest 実行時してはじめて、以下のようなエラーが表示されました。

e: string; property?: string | undefined; }>'.` ``

コードを書いてくれている時に教えてくれるもの、実行時に教えてくれるものもあり、その違いについては現時点でわかっておりません。Linterで教えてくれるだけでも助かります。

# type TypeKey<T> = { \[K in keyof T]: K }\[keyof T]  を分解して解読する

{ [K in keyof T]: K }

ここは、Mapped Types部分で、上の例よりシンプルですね。

{[Key in keyof Type]: undefined extends Type[Key] ? never : Key}

は三項演算子でかかれており条件によっては、`never` 値を返さない型(つまり省略される)となっていましたが、

コロンのあとに `K` と書かれているので特に条件はないのでシンプルに、`: K` となっています。

つづいて、
\[keyof T] これは型のプロパティ名を列挙していますね。

以下は、擬似コードですが自分は以下のように解釈して腹落ちしました。

// 型を作成 type map = { [K in keyof T]: K } map /*_ { id : id, name: name, point: point } _/

// Lookup Type によりプロパティの型を取得 { id : id, name: name, point: point }[keyof User] /*_ id, name, point _/

つまり、

type TypeKey = { [K in keyof T]: K }[keyof T] func hoge(key: TypeKey) { … }

func hoge(key: “id” | “name”) { … }

一箇所のみで使用されるのであれば下記のように明示的に書いたほうが可読性は高いとは思いますが、
プロパティ名が増える or 複数箇所で使われるようなら、前者を使うほうがベターですね。

テストしてみると、しっかり型に含まれているプロパティ名のみを受け取る引数として機能していることがわかります。

type TypeKey = { [K in keyof T]: K }[keyof T]

function echoTypeKey(key: TypeKey): string { return key.toString() }

test(“typeKey”, () => { const message = echoTypeKey(“name”) // const message = echoTypeKey(“name”) // id と name 以外の Key を受け付けない TypeScript がチェックしてくれる expect(message).toStrictEqual(“name”) })

# Reference

* [Mapped types](https://github.com/Microsoft/TypeScript/pull/12114)
* [Notes on TypeScript: Mapped Types and Lookup Types](https://dev.to/busypeoples/notes-on-typescript-mapped-types-and-lookup-types-i36)