8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

keyof と typeof をすこ〜し細かく解説!後輩の質問から得たTypeScriptの知識

Last updated at Posted at 2024-09-23

こんにちはゆせです!先日後輩からkeyof, typeofについて結構聞かれて、即答できない部分があったのでここにアウトプットしておきます!!後輩ちゃんありがとう笑

本記事で解像度が高くなるであろうものたち

上記はすべてサバイバルTypeScriptに飛ぶようにしています。ではさっそくコード含めて見ていきましょう!

keyofについて

keyof.ts
type TMember = {
  name: string,
  grade: number,
};

type TMemberKey = keyof TMember // TMemberKey =  "name" | "grade"

keyofについて簡単にまとめます

  • オブジェクトのプロパティ名(key-valueのkey)を取得できる
  • ユニオン型で取得できる
  • 」に対して使用できる(「変数」には使用できない)

上記のコードだとあまりメリットが湧かないかもですが、サバイバルTypeScriptにはこう書かれています。

プロパティが何十個もあるようなオブジェクトを想像してみてください。そのプロパティ名のユニオン型を定義する必要が出てきたとします。その際に、プロパティ名をすべて転記するとなると、転記漏れや書き間違いもあるでしょう。そういう場合はkeyofを使うとそもそも書き写す必要がないため、便利な上に安全なコーディングができます。

確かに納得です。TMemberには上記の通りプロパティは2つだけですが、仮にこれが20個あるとして、これをユニオン型で定義しないといけないとなった時、保守性、可読性の観点からするとkeyofは便利ですね。keyofの一撃必殺で20個のユニオン型が出来上がります。

実際に使われるケース

keyof.ts
type TMember = {
  name: string,
  grade: number,
};

type TMemberSurvey = {
  techSkill: number,
  communicationSkill: number,
  techLanguage: string,
};

type TFormTitle = keyof TMember | keyof TMemberSurvey;
// TFormTitle = "name" | "grade" | "techSkill" | "communicationSkill" | "techLanguage"

type TFormInput = "text" | "number";

type TForm = {
  title: TFormTitle, 
  type: TFormInput,
  // titleはname, grade, techSkill, communicationSkill, techLanguageに限定される
  // typeはtextかnumberに限定される
};

const form_v1: TForm = {
  title: "techSkill",
  type: "text",
};

// もしくは
const form_v2: TForm[] = [
  { title: "name", type: "text" },
  { title: "grade", type: "number" },
  { title: "techSkill", type: "number" },
  { title: "communicationSkill", type: "number" },
  { title: "techLanguage", type: "text" },
  // { title: "apple", type: "text" }, ←titleにappleは含まれていないのでコンパイルエラー
];

一見上記のコードは色々書いてしまっているので複雑に見えますが、上からちゃんと読んでみると案外簡単なものです。

typeofについて

typeof.ts
const member = {
  name: "太郎",
  age: 21,
};

type TMember = typeof member;
// TMember = {name: string; age: number;}

typeofについて簡単にまとめます

  • JavaScriptのtypeof演算子とは別物
  • 宣言済みの「変数」の型を取得できる
    • 変数」に対して使用できる(「型」には使用できない)

一言で表すと、変数から型を抽出する型演算子です。変数の値がtrueであればbooleanを取得してくれるということです。

実際に使われるケース

typeof.ts
const language = ["JavaScript", "TypeScript", "Python", "Java", "C#"] as const;
type TLanguage = (typeof language)[number];
// TLanguage = "JavaScript"  "TypeScript" | "Python" | "Java" | "C#"

type TMember = {
  name: string,
  language: TLanguage,
};

const member: TMember = {
  name: "ゆせ",
  language: "TypeScript", //TLanguageにないものはコンパイルエラー
};

後輩ちゃんに解説してあげられなかったやつ!

const language = ["JavaScript", "TypeScript", "Python", "Java", "C#"] as const;
type TLanguage = (typeof language)[number];

この2行を後輩に聞かれ即答できませんでした。。いつも感覚で読んで感覚でコードを書いているのを反省しています😇解説します!

constアサーション「as const」

readonly(読み取り専用、変更不可)のタプル型で取得できます。そもそも今回の目的はlanguageの値("JavaScript", "TypeScript"...)自体を型に導く(つまりユニオン型にする)ためのファーストステップとしてas constを使っています。仮に使わないとなると、型推論で以下のようにただのstring型の配列とみなされてしまいます。

as const.ts
const arrLanguage = ["JavaScript", "TypeScript", "Python", "Java", "C#"];
// arrLanguageの型 = string[]
const language = ["JavaScript", "TypeScript", "Python", "Java", "C#"] as const;
// languageの型 = readonly ["JavaScript", "TypeScript", "Python", "Java", "C#"]

タプル型とインデックスアクセス型

さて、では上で取得したreadOnlyのタプル型languageをユニオン型(TLanguage = "JavaScript" | "TypeScript" | "Python" | "Java" | "C#")で取得したいです。

indexAccess.ts
type TLanguage = (typeof language)[number];

答えは上記の通りなのですが、パッとわかりにくいので一つずつ見ていきましょう。

  1. (typeof language)
    変数languageの型を取得します。つまり上でやった通り、readonly ["JavaScript" | "TypeScript" | "Python" | "Java" | "C#"]の取得になります。constアサーションのおかげでこの型を取得できています👍
  2. [number]
    なんやこれ?と思った人もいるでしょう。タプルに対して [number] を使うと、全てのインデックスの要素の型をユニオン型で取得します。
    つまりは、超わかりやすく書くと

language[0] の型:"JavaScript"
language[1] の型:"TypeScript"
language[2] の型:"Python"
language[3] の型:"Java"
language[4] の型:"C#"

結果、

language.ts
type TLanguage = (typeof language)[number];
// TLanguage = "JavaScript" | "TypeScript" | "Python" | "Java" | "C#"

となるわけです!

タプル型とインデックスアクセス型についてはこちら

extends君

extends.ts
const member = {
  name: "太郎",
  age: 21,
}
type TMember = typeof member;

interface TUpdateMember extends TMember {
  grade: number;
}
/*
TUpdateMember = {
  name: string,
  age: number,
  grade: number,
}
*/

// type定義はextends君使えないからこれ↓↓↓
type TUpdateMember = TMember & {
  grade: number;
};

extendとは英語で「拡大する、伸ばす」という意味がありますが、typescriptでも同じように型を拡張しているイメージですかね。extendsinterface定義で使用することができ、type定義であれば&を使います。

keyof, typeof一緒に使うパターンもあるぞおお

実際に使われるケースその1

keyof&&typeof.ts
const member = {
  name: "ゆせ",
  age: 21,
  language: "TypeScript",
};

type TMember = keyof typeof member;
// TMember = "name" | "age" | "language"

今回やりたいことが、「変数のプロパティ名の取得」だとします。つまり、nameやageを取得したいということです。
おさらいになりますが、プロパティ名の取得はkeyofでできるものの、型にのみ使用可能でしたね。変数には使用できません。memberは変数なので、直接keyofのみで取得することはできません。

じゃあどうすればいいかと言うと、変数memberの型を取得し、それをkeyofに当ててあげればいいわけです!イメージ↓↓

  1. typeof memberでmemberの型を取得
    name:string, age:number, ...といった型を取得できます。
  2. 1で取得した型をkeyofに当てる
    そうすれば、name, age, languageを取得できますね!

1と2を合体させたコードが

keyof&&typeof.ts
type TMember = keyof typeof member;

になります!英語や普段のプログラミングと同じで、直感的にわからなければ分解していくと少しわかりやすいですね!次はもう少し複雑です

実際に使われるケースその2

keyof&&typeof.ts
const member = {
  name: "太郎",
  age: 21,
  language: "TypeScript",
} as const;

type TMember = (typeof member)[keyof typeof member];
// TMember = "太郎" | 21 | "TypeScript"

いかがでしょう。今まで解説した通りにちゃんと考えることができればTMemberの実態がわかるかと思いますが、とは言っても直感的にはわかりにくいものなので解説していきます。先ほどと同様、分解して見ていきましょう!

  1. ここでのtypeof member
    memberにはas constがついていますね。読み取り専用readOnlyになります。先ほどは配列の中身をそのままで取得していましたが、今回はどうでしょう。以下がその結果です。
keyof&&typeof.ts
const member = {
  name: "太郎",
  age: 21,
  language: "TypeScript",
} as const;

/* ↓memberの型を見てみると、、
member = {
 readonly name: "太郎";
 readonly age: 21;
 readonly language: "TypeScript";
}

as constがなければ、、
member = {
 name: string;
 age: number;
 language: string;
}
*/

今回も同様、そのまま読み取り専用で取得していますね。constアサーションがついたmemberの型は、memberそのものを型としてるのです。as constがなければ、上記の通りそれぞれの型をプリミティブに取得します。

2. [keyof typeof member]を忘れずに
先ほど上でやったことと全く同じです。一言で言うと、変数の中のプロパティ名を取得します。つまりここで言う、name, age等の取得をしています。

3. 1と2でやったことを合体しよう!(typeof member)[keyof typeof member]
この形(typeof member)[keyof typeof member] ≒ (型[キー])はインデックスアクセス型です。全てのインデックスの要素の型をユニオン型で取得します。結論から見た方が早いのでわかりやすく書きます。

member[name]"太郎"
member[age]"21"
member[language]"TypeScript"

memberは読み取り専用そのものの型です。これは1でやったことです。
それにインデックス番号をつけてあげたく、今回はインデックス番号というの名の、プロパティ名を指定してあげるとその値を取得できるということですね。プロパティ名を取得し指定している部分([name]とか)が2でやったことです。

インデックスアクセス型は特定の値を指定しない限りすべて取得するんでしたよね。今回はそれに該当するので、すべて取得し、結果

keyof&&typeof.ts
const member = {
  name: "太郎",
  age: 21,
  language: "TypeScript",
} as const;

type TMember = (typeof member)[keyof typeof member];
// TMember = "太郎" | 21 | "TypeScript"

となります!

まとめ

かなり細かく解説しているところもあり、文章が長くなってしまいました申し訳ないです🙇‍♂️

とにかくこの記事広範囲でkeyof,typeofを使ったと思います!私が書いた解説を読みながら、「あれ?typeofってなんだっけ?」なんてこともあるかと思います。そんな時は初めに解説した部分に戻って都度確認しにいってください!
私もこの記事を書く前よりkeyofとtypeofの解像度が高まりました、考える、触れる反復がとにかく大事だと実感できました。

この記事が誰かのお役に立てれば嬉しいです!

8
7
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
8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?