動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

C# 出身者のための TypeScript mapped types と keyof(既存の型から型を作る)

バイブス父さん
現役の業務SE
2026年9月15日12 min read広告 (PR) を含む場合があります
C# 出身者のための TypeScript mapped types と keyof(既存の型から型を作る)

みなさんこんにちは!ヒロポンです!

C# でプロパティを機械的に扱いたいとき、私はずっとリフレクション頼みでした。Type.GetProperties() で全プロパティを取る。あとは foreach で回す。あの手癖です。

で、TypeScript を触り始めると Partial<User>(全プロパティを省略可能にした型)みたいな組み込みのユーティリティ型にぶつかります。便利。めちゃくちゃ便利。便利なんだけど、ある日ふと「これ、中身どうなってるん?? 自分でも似たの作れるんかな」と気になったんですよね。

その答えが TypeScript mapped types keyof です。

keyof で型のキー一覧を取る。[K in keyof T] で全プロパティを走査する。そこに ?readonly を付け外しする。これだけで Partial みたいな型を自分の手で組み立てられます。

面白いのは、そのタイミング。C# のリフレクションが実行時にやることを、TypeScript はコンパイル時に型としてやる。今回はこの対比を軸に、C# 出身の目線で追っていきます。

C# のリフレクション と TypeScript の型操作、まず対応マップ

先に全体像を置いておきます。「C# でこう書いてたこと」が「TS だとこれ」に対応する、という地図です。

プロパティ名一覧・全プロパティ走査・全部省略可能な型・いつ効くか、をC#リフレクションとTypeScript型操作で対応させた比較表

一番の違いは最後の行です。C# のリフレクションは動かしてみないと分からない。一方の TS は、コードを書いた瞬間にエディタが赤線を出してくれる。この差が、いい感じに効いてくるんですよね!!

では keyof から順に。

keyof — 型のキー一覧を取る

keyof は、型の全プロパティ名を文字列リテラルのユニオン型として取り出します。

interface User {
  id: number;
  name: string;
  age: number;
}

type UserKeys = keyof User;   // "id" | "name" | "age" という型
const k: UserKeys = "name";   // OK。"xxx" みたいな存在しないキーは型エラー

発想としては C# の Type.GetProperties() に近い。ただ、返ってくるのは実行時の配列ではなく型です。だから k"id" "name" "age" 以外を入れようとすると、その場で型エラー。存在しないプロパティ名を弾ける、というわけです。

ここまでで「keyof はキー名の一覧を"型として"取るやつ」。まずこれだけ押さえておけばOKです。

[K in keyof T] — 全プロパティを走査する

keyof で取ったキーを使って、全プロパティを1つずつ走査しながら新しい型を組み立てる。これが mapped types です。書き方は [K in keyof T]

// 全プロパティを省略可能(?)にする → Partial の自作版
type MyPartial<T> = { [K in keyof T]?: T[K] };

const p: MyPartial<User> = { name: "太郎" };   // id も age も省略OK

[K in keyof T] は「T の各キー K について」という意味。C# の foreach (var prop in type.GetProperties()) を、型のレベルでやってるイメージです。T[K] は「そのキーの値の型」(name なら string)。

手で書いた場合と並べると、ありがたみがはっきりします。

更新用の省略可能な型を手で書く版と、mapped typeで元の型から自動追従させる版のdiff

Before は、User にプロパティが増えるたびに手で足す。足し忘れると事故る。After は元の型を見に行くので、追従が勝手に効きます。

これ、実は組み込みの Partial<T> とほぼ同じ定義なんです。つまり Partial の中身を自分で書けた、ということ。こんな感じで正体が見えてくると、ちょっと嬉しくなりますよね。

ここまでで「[K in keyof T] は全プロパティを走査して型を作るやつ」まで来ました。

修飾子(? / readonly)を付けたり外したりする

mapped types の強みは、走査しながら修飾子を付け外しできるところです。

  • 付ける: ?(省略可能に)、readonly(読み取り専用に)
  • 外す: -?(必須に戻す)、-readonly(可変に戻す)
// readonly を付与 → Readonly の自作版
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };

// -? で全部を必須に戻す → Required の自作版
type MyRequired<T> = { [K in keyof T]-?: T[K] };

// -readonly で readonly を外して可変に
type Mutable<T> = { -readonly [K in keyof T]: T[K] };

? を素で書けば「付与」。頭に - を足せば「除去」。これで PartialRequiredReadonly も、いい感じに自分の手で作れてしまいます。

ここまでで、修飾子の付け外しまで含めて、既存の型から新しい型を機械的に作れるようになりました。

ミニマム検証 — 手元の tsc で確かめる

型の話は、実際に tsc(TypeScript コンパイラ)に通すと一気に腹落ちします。次のコードを1ファイルに貼って、tsc --noEmit --strict で型チェックしてみてください。

interface User { id: number; name: string; age: number; }

type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type Mutable<T>    = { -readonly [K in keyof T]: T[K] };

const r: MyReadonly<User> = { id: 1, name: "花子", age: 30 };
// r.id = 2;   // ← ここのコメントを外すと readonly でエラーになる

const m: Mutable<MyReadonly<User>> = { id: 2, name: "x", age: 1 };
m.id = 99;     // Mutable で readonly を外したので、これは書き換えOK

r.id = 2; のコメントを外すと、error TS2540: Cannot assign to 'id' because it is a read-only property. と、コンパイル時に弾かれます。動かす前に分かる。ここがリフレクションとの決定的な違いです。

ハマりポイント

型操作は、C# の感覚のまま踏み込むと引っかかる所があります。私が引っかかったのは、大きく2つ。

ハマりポイント①: keyof が返すのは「値」ではなく「型」。

C# の GetProperties() は実行時に PropertyInfo の配列を返すので、そのままループで回せます。でも keyof User が作るのは "id" | "name" | "age" という。実行時に使える文字列配列ではありません。

正直、私も最初これをリフレクションの感覚で捉えてて、「keyof の結果って実行時に for で回せるんちゃうん??」と思って手が止まりました。回したいなら実行時の値、つまり Object.keys(obj) を使う話。keyof はあくまで型チェック用です。コンパイル時の型と実行時の値は別物、とスイッチを切り替える必要があります。

ハマりポイント②: 修飾子の -?? を逆に覚える。

? は「付与」、-? は「除去」。ここ、直感と逆に感じる人が多いです。「マイナスだから省略可能にする(減らす)」とイメージすると、逆を書いてしまう。- は"その修飾子を外す"、と覚えると間違えません。-? は「? を外す」、つまり必須化です。

私の現場メモ

C# 出身で TypeScript に来て、私が一番「おっ」となったのが、この型操作でした。

C# だと、DTO のプロパティ構成を動的に扱いたいときはリフレクション頼み。書けるんだけど、実行時まで結果が分からないし、コードもそれなりに長くなる。TS の mapped types は、それを型の世界で、しかも数行で書ける。API のレスポンス型から「更新用に全部省略可能にした型」を1行で派生させる。そういう場面でよく使います。

とはいえ、C# のリフレクションが劣ってるわけじゃない。C# は実行時に本当に動的なこと(プラグイン読み込みや属性ベースの処理)ができます。TS の型操作はコンパイル時に型を固めるのが得意。要は守備範囲が違うだけ。どっちも道具として持っておくと、現場で「これはどっちの世界の話か」を切り分けられて強いです。

まとめ

TypeScript の keyof と mapped types は、既存の型から新しい型を機械的に作る仕組みでした。

  • keyof T: 型のキー名を"型として"取り出す("id" | "name" | ...)。
  • [K in keyof T]: 全プロパティを走査して型を組み立てる。
  • 修飾子: ? readonly で付与、-? -readonly で除去。

これで PartialReadonly の正体が見えて、必要なら自分でも作れる。そして軸になるのは、C# のリフレクションが実行時なのに対し、TS はこれをコンパイル時の型操作でやる、という違いでした。

型で守れる範囲が広がると、書いた瞬間にミスに気づける。後工程のバグが減る。C# で培った「型を大事にする」感覚は、TypeScript でこそ活きます。この型操作まで手に入れておくと、フロントや Node の案件が来ても「型は読める・組める」と言える。次のキャリアの選択肢が、ひとつ広がりますよ!!

よくある質問

keyof の結果を実行時にキーの配列として使えますか?

そのままでは使えません。keyof T はコンパイル時の型です。実行時にキーの配列が欲しいなら Object.keys(obj) を使います。ただし Object.keys の戻り値は string[] になるので、厳密に型を効かせたいときは as (keyof T)[] で型注釈を足す、という合わせ技をよく使います。

Partial や Readonly が最初からあるのに、自作する意味はありますか?

組み込みで足りるなら、それで十分です。自作の意味が出るのは「特定のキーだけ optional にする」「値の型も同時に変換する」みたいに、組み込みユーティリティ型では足りない独自の変換をしたいとき。仕組みを理解しておくと、組み込みで足りない場面でも詰まらなくなります。

mapped types はどのバージョンから使えますか?

mapped types と keyof は TypeScript 2.1 から、-?-readonly の修飾子除去は 2.8 からで、いずれも今のバージョンでは普通に使えます。キー名を変換する as を使った key remapping は 4.1 からなので、そこだけはバージョンに注意です。

T[K] の書き方は何ですか?

インデックスアクセス型と呼びます。「型 T のプロパティ K の値の型」を取り出す書き方で、User["name"] なら string になります。mapped types の中で T[K] と書くと、走査中のキー K に対応する値の型をそのまま引き継げます。

次に読むべき記事

以上!


執筆者

バイブス父さん — 業務 SE 7 年 (SIer 正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SIer の正社員からフリーランスに転じ、 複数のエージェント経由で案件を回してきた経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について