Mapped Types とは「ある型のキーを1つずつ取り出して、新しいオブジェクト型を組み立てる構文」、Conditional Types とは「型が条件に合うかどうかで結果の型を切り替える構文」です。 前者は { [K in keyof T]: ... }、後者は T extends U ? X : Y の形で書き、この2つを組み合わせると Partial や Omit、ReturnType といった組み込みのユーティリティ型をすべて自分で書けるようになります。

この記事では、keyof・in・extends・infer・as と修飾子(-?、-readonly)の早見表を先頭に置き、Partial・Pick・Omit・ReturnType・Awaited を自作して「変換前 → 変換後の型」を並べます。載せているホバー結果とエラー文は、すべて TypeScript 5.9.3 で取り直したものです。 ユニオン分配と never の挙動、タプルで包んで分配を止める書き方、Template Literal Types を使ったキー名の変換まで、コピーしてそのまま試せるコードで順に確認していきます。

Mapped Types と Conditional Types の構文早見表

まず、型を変換するときに使う部品の一覧です。型レベルの処理は、この7つの部品の組み合わせでできています。

部品 書き方 働き 例と結果
keyof keyof T キー名のユニオンを取り出す keyof { id: number; name: string } は "id" | "name"
in [K in Keys] ユニオンを1つずつ回してプロパティを作る { [K in "a" | "b"]: K } は { a: "a"; b: "b" }
extends(条件) T extends U ? X : Y T が U に代入できれば X、できなければ Y "a" extends string ? 1 : 2 は 1
extends(制約) K extends keyof T 型引数に渡せる型を絞る Pick の第2引数をキー名だけに限定
infer T extends (infer E)[] ? E : never 条件にマッチした一部分を取り出して名前を付ける string[] から string を取り出す
as [K in keyof T as NewKey] キー名を付け替える。never にするとそのキーが消える id を getId に変える
修飾子 ? -? readonly -readonly 省略可能・読み取り専用を付ける、外す -? で省略可能を外す

組み込みのユーティリティ型は、次のように部品へ分解できます。

ユーティリティ型 使う部品 自作の定義
Partial<T> keyof・in・? { [K in keyof T]?: T[K] }
Required<T> keyof・in・-? { [K in keyof T]-?: T[K] }
Readonly<T> keyof・in・readonly { readonly [K in keyof T]: T[K] }
Pick<T, K> in・extends(制約) { [P in K]: T[P] }
Record<K, V> in { [P in K]: V }
Exclude<T, U> extends(条件)・分配 T extends U ? never : T
Extract<T, U> extends(条件)・分配 T extends U ? T : never
Omit<T, K> in・as・extends(条件) { [P in keyof T as P extends K ? never : P]: T[P] }
ReturnType<T> extends(条件)・infer T extends (...args: any) => infer R ? R : never
Parameters<T> extends(条件)・infer T extends (...args: infer P) => any ? P : never
Awaited<T> extends(条件)・infer・再帰 T extends PromiseLike<infer U> ? Awaited<U> : T

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「TypeScript Mapped/Conditional Types早見表」

確認した環境と、ホバー結果の取り方

この記事の結果は次の環境で確認しました。

項目 内容
TypeScript 5.9.3(npm の typescript パッケージ)
Node.js 22 系
コンパイラ設定 strict: true、target: ES2022
ホバー結果の取得 TypeScript の Language Service API(getQuickInfoAtPosition)
エラー文の取得 npx tsc --noEmit --strict --target es2022 ファイル名

エディタで型名にマウスを重ねると出る表示(ホバー)は、TypeScript の Language Service が返している文字列です。同じ API をスクリプトから呼ぶと、ホバーと同じ内容をターミナルに一覧で出せます。 エディタ上での目視はしておらず、この記事のホバー結果はすべて次のスクリプトの出力です。

mkdir ts-types && cd ts-types
npm init -y
npm install typescript@5.9.3
// show.cjs
// node show.cjs sample.ts  → type で始まる行の型をホバーと同じ形式で表示する
const ts = require("typescript");
const fs = require("fs");
const file = process.argv[2];
const options = { strict: true, target: ts.ScriptTarget.ES2022, noErrorTruncation: true };
const host = {
  getScriptFileNames: () => [file],
  getScriptVersion: () => "1",
  getScriptSnapshot: (f) =>
    fs.existsSync(f) ? ts.ScriptSnapshot.fromString(fs.readFileSync(f, "utf8")) : undefined,
  getCurrentDirectory: () => process.cwd(),
  getCompilationSettings: () => options,
  getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
  fileExists: ts.sys.fileExists,
  readFile: ts.sys.readFile,
};
const service = ts.createLanguageService(host);
const source = fs.readFileSync(file, "utf8");
const pattern = /^type (\w+) =/gm; // 型引数のない type だけを対象にする
let m;
while ((m = pattern.exec(source))) {
  const info = service.getQuickInfoAtPosition(file, m.index + 5);
  console.log(ts.displayPartsToString(info.displayParts).replace(/\n\s*/g, " "));
}
// sample.ts
type User = { id: number; name: string; email?: string; readonly createdAt: Date };
type MyPartial<T> = { [K in keyof T]?: T[K] };
type Result = MyPartial<User>;
$ node show.cjs sample.ts
type User = { id: number; name: string; email?: string; readonly createdAt: Date; }
type Result = { id?: number | undefined; name?: string | undefined; email?: string | undefined; readonly createdAt?: Date | undefined; }

以降のコードでは、結果の型を // → のコメントで横に書きます。この User 型を変換前の型として使い回します。

type User = { id: number; name: string; email?: string; readonly createdAt: Date };

Mapped Types の基本:キーを回して新しい型を作る

Mapped Types は、「キーのユニオンを in で1つずつ回し、それぞれに値の型を割り当てる」 構文です。配列の map に近い考え方で、型のプロパティを1件ずつ作り直します。

type Flags = { [K in "a" | "b"]: K };
// → { a: "a"; b: "b"; }

K はループ変数で、1周目は "a"、2周目は "b" が入ります。ここに keyof T と T[K](T のキー K の値の型)を組み合わせると、既存の型を土台にした変換になります。

type Copy<T> = { [K in keyof T]: T[K] };   // そのまま写す
type Nullable<T> = { [K in keyof T]: T[K] | null };  // 値に null を足す
type Stringify<T> = { [K in keyof T]: string };      // 値をすべて string にする

type R16 = Nullable<User>;
// → { id: number | null; name: string | null; email?: string | null | undefined; readonly createdAt: Date | null; }
type R17 = Stringify<User>;
// → { id: string; name: string; email?: string | undefined; readonly createdAt: string; }

結果を見ると、email の ? と createdAt の readonly が変換後にも残っています。 [K in keyof T] の形で書いた Mapped Types は、元の型の修飾子を引き継ぐためです(この性質を持つものを準同型の Mapped Types と呼びます)。

Partial・Required・Readonly を自作して修飾子を付け外しする

修飾子は、付けたいときはそのまま書き、外したいときは先頭に - を付けます。

type MyPartial<T>  = { [K in keyof T]?: T[K] };           // ? を付ける
type MyRequired<T> = { [K in keyof T]-?: T[K] };          // ? を外す
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };   // readonly を付ける
type Mutable<T>    = { -readonly [K in keyof T]: T[K] };  // readonly を外す

type R02 = MyPartial<User>;
// → { id?: number | undefined; name?: string | undefined; email?: string | undefined; readonly createdAt?: Date | undefined; }
type R03 = MyRequired<User>;
// → { id: number; name: string; email: string; readonly createdAt: Date; }
type R04 = MyReadonly<User>;
// → { readonly id: number; readonly name: string; readonly email?: string | undefined; readonly createdAt: Date; }
type R05 = Mutable<User>;
// → { id: number; name: string; email?: string | undefined; createdAt: Date; }
書き方 意味 対応する組み込み型
[K in keyof T]?: すべて省略可能にする Partial
[K in keyof T]-?: 省略可能を外して必須にする Required
readonly [K in keyof T]: すべて読み取り専用にする Readonly
-readonly [K in keyof T]: 読み取り専用を外す 組み込みにはない(自作する)

-? を付けると、R03 のように email の型から undefined も一緒に外れます。なお +? や +readonly という書き方もあり、+ を省略したときと同じ意味です。readonly を外す Mutable は組み込みに用意されていないので、この1行を覚えておくと便利です。

Pick・Record を自作する

Pick は、回すキーを keyof T 全体ではなく、引数で受け取った K に絞ったものです。

type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyRecord<K extends keyof any, V> = { [P in K]: V };

type R06 = MyPick<User, "id" | "name">;
// → { id: number; name: string; }
type R08 = MyRecord<"draft" | "published", number>;
// → { draft: number; published: number; }

K extends keyof T は条件分岐ではなく 型引数の制約 です。「K には T のキー名しか渡せない」という意味で、これを外すと定義の時点で次のエラーになります。

type MyPickLoose<T, K> = { [P in K]: T[P] };
err.ts(2,34): error TS2322: Type 'K' is not assignable to type 'string | number | symbol'.
err.ts(2,38): error TS2536: Type 'P' cannot be used to index type 'T'.

1つめは「in の右側に置けるのはキーになれる型(string・number・symbol)だけ」、2つめは「P が T のキーとは限らないので T[P] と書けない」という指摘です。keyof any は string | number | symbol のことで、Record のように「キーになれる型なら何でもよい」と書きたいときに使います。

修飾子が引き継がれる形と、引き継がれない形

同じ内容に見える2つの Mapped Types でも、in の右側の書き方で結果が変わります。

type Homomorphic    = { [K in keyof User]: User[K] };
// → { id: number; name: string; email?: string | undefined; readonly createdAt: Date; }
type NonHomomorphic = { [K in "id" | "name" | "email" | "createdAt"]: User[K] };
// → { id: number; name: string; email: string | undefined; createdAt: Date; }

キー名を文字列のユニオンで直接並べると、? と readonly は引き継がれません。 email は「省略可能」ではなく「必須だが値は undefined でもよい」に変わり、createdAt は書き込み可能になります。元の型の性質を保ちたいときは keyof T(または Pick のように keyof T を制約にした型引数)を in の右側に置きます。

Conditional Types の基本:extends で型を分岐する

Conditional Types は、型の世界の三項演算子です。T extends U ? X : Y は「T が U に代入できるなら X、できないなら Y」 と読みます。

type IsString<T> = T extends string ? "yes" : "no";

type D01 = IsString<"a">;      // → "yes"
type D05 = IsString<unknown>;  // → "no"
type D11 = IsString<boolean>;  // → "no"

ここでの extends は「継承」ではなく「代入できるか」の判定です。"a" は string に代入できるので "yes"、unknown は string に代入できないので "no" になります。

Exclude・Extract・NonNullable を自作する

条件に合わないときに never を返すと、ユニオンから特定のメンバーを取り除く型になります。

type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type MyNonNullable<T> = T extends null | undefined ? never : T;

type R09 = MyExclude<"a" | "b" | "c", "a">;
// → "b" | "c"
type R10 = MyExtract<string | number | boolean, number | boolean>;
// → number | boolean
type R11 = MyNonNullable<string | null | undefined>;
// → string

"a" | "b" | "c" 全体は "a" に代入できないのに、結果が "b" | "c" になるのは不思議に見えます。これは次の章で扱う ユニオン分配 が働いているためです。

ユニオン分配:ユニオン型は1つずつ判定される

型引数 T をそのまま extends の左側に置いた Conditional Types は、T にユニオン型が渡されると、メンバーごとに判定して結果をユニオンでまとめ直します。 これをユニオン分配(Distributive Conditional Types)と呼びます。

type IsString<T> = T extends string ? "yes" : "no";

type D02 = IsString<string | number>;
// → "yes" | "no"
flowchart LR
  A["IsString に string | number を渡す"] --> B["string を判定 → yes"]
  A --> C["number を判定 → no"]
  B --> D["結果をユニオンにまとめる → yes | no"]
  C --> D

Exclude が動く理由もこれで説明できます。MyExclude<"a" | "b" | "c", "a"> は、"a" → never、"b" → "b"、"c" → "c" と1つずつ判定され、never | "b" | "c" がまとめられて "b" | "c" になります(never はユニオンの中では消えます)。

分配のおかげで、ユニオンの各メンバーを個別に変換できます。

type ToArray<T> = T extends any ? T[] : never;

type D06 = ToArray<string | number>;
// → string[] | number[]

分配が起きる条件と起きない条件

分配は「いつでも起きる」わけではありません。起きるのは、型引数がそのままの形で extends の左側にあるときだけです。

書き方 分配 string | number を渡した結果
T extends string ? "yes" : "no" 起きる "yes" | "no"
[T] extends [string] ? "yes" : "no" 起きない "no"
{ value: T } extends { value: string } ? "yes" : "no" 起きない "no"
(string | number) extends string ? "yes" : "no"(型引数を使わず直接書く) 起きない "no"
type D18 = (string | number) extends string ? "yes" : "no";
// → "no"   型引数を通していないので、ユニオン全体で1回だけ判定される

type Wrapped<T> = { value: T } extends { value: string } ? "yes" : "no";
type G01 = Wrapped<string | number>;
// → "no"   T が別の型に包まれているので分配されない

boolean と any も分かれて判定される

boolean は内部的に true | false のユニオンなので、分配の対象になります。

type IsTrue<T> = T extends true ? "T" : "F";

type D12 = IsTrue<boolean>;
// → "F" | "T"

any を渡したときは、条件の両方の結果がユニオンで返ります。

type D04 = IsString<any>;
// → "yes" | "no"

「必ずどちらか一方が返る」と思って書いた型が両方を返していたら、boolean か any が渡っていないかを確かめると原因が見つかります。

never の挙動:分配すると結果も never になる

never は「メンバーが0個のユニオン」として扱われます。分配が起きる Conditional Types に never を渡すと、判定する対象が1つもないので、条件の中身に関係なく結果は never になります。

type D03 = IsString<never>;
// → never   ("yes" でも "no" でもない)

type IsNeverNaive<T> = T extends never ? true : false;
type D09 = IsNeverNaive<never>;
// → never   (true にならない)

never かどうかを判定する型を素直に書くと、true でも false でもなく never が返ってきます。ここが Conditional Types でいちばんつまずきやすい箇所です。

never のその他の性質も、まとめて確認しておきます。

式 結果 説明
string | never string ユニオンの中の never は消える
string & never never 交差型に never があると全体が never
keyof never string | number | symbol どんなキーでも持っている扱いになる
IsString<never> never 分配の対象が0個なので結果も never
Mapped Types の as で never を返す そのキーが消える キーの絞り込みに使う

「ユニオンの中で消える」「as で返すとキーが消える」という2つの性質が、Exclude や Omit の土台になっています。

タプルで包んで分配を止める

分配させたくないときは、extends の両側を角かっこで包み、[T] extends [U] と書きます。 1要素のタプル同士の比較になり、T がそのままの形ではなくなるので、ユニオン全体で1回だけ判定されます。この書き方は TypeScript の公式ハンドブック(Conditional Types のページ)にも載っています。

type ToArray<T>       = T extends any ? T[] : never;
type ToArrayNoDist<T> = [T] extends [any] ? T[] : never;

type D06 = ToArray<string | number>;
// → string[] | number[]      文字列だけの配列、または数値だけの配列
type D07 = ToArrayNoDist<string | number>;
// → (string | number)[]      文字列と数値が混ざってよい配列

never の判定も、この形にすると期待どおりに動きます。

type IsNever<T> = [T] extends [never] ? true : false;

type D08 = IsNever<never>;   // → true
type D10 = IsNever<string>;  // → false

「ユニオンのすべてのメンバーが条件を満たすか」を調べたいときにも使えます。

type AllString<T> = [T] extends [string] ? "yes" : "no";

type F10 = AllString<string | number>;  // → "no"   number が混ざっている
type F11 = AllString<"a" | "b">;        // → "yes"  全メンバーが string に代入できる
type F12 = AllString<never>;            // → "yes"  never は何にでも代入できる
やりたいこと 書き方
ユニオンのメンバーごとに変換・絞り込みをしたい T extends U ? X : Y(分配させる)
ユニオン全体をまとめて1回だけ判定したい [T] extends [U] ? X : Y
never かどうかを判定したい [T] extends [never] ? true : false
分配だけを目的にメンバーを1つずつ回したい T extends any ? ... : never

4つめの T extends any ? ... : never は、条件としては常に真ですが、分配を起こすためのきっかけ として使う定番の形です。次の例では、ユニオンの各メンバーのキーをすべて集めています。

type KeysOfUnion<T> = T extends any ? keyof T : never;

type G03 = keyof ({ a: 1; b: 2 } | { a: 3; c: 4 });
// → "a"               ユニオンに keyof を直接使うと、共通するキーだけになる
type G04 = KeysOfUnion<{ a: 1; b: 2 } | { a: 3; c: 4 }>;
// → "a" | "b" | "c"   メンバーごとに keyof を取ってまとめる

infer:マッチした部分の型を取り出す

infer は、extends の右側のパターンの中に「ここに入る型を取り出して名前を付ける」という穴を開けるキーワード です。正規表現のキャプチャに近い働きをします。使えるのは Conditional Types の extends の右側だけで、取り出した型は真の側(? の直後)で使えます。

type ElementOf<T> = T extends readonly (infer E)[] ? E : never;

type I01 = ElementOf<string[]>;
// → string
type I02 = ElementOf<readonly [1, "a", true]>;
// → true | "a" | 1

「T が何かの配列なら、その要素の型を E と呼ぶ。そして E を返す」という読み方になります。

ReturnType・Parameters を自作する

関数の型のうち、戻り値の位置に infer を置けば ReturnType、引数の位置に置けば Parameters になります。

type MyReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : never;
type MyParameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

declare function fetchUser(id: number, withPosts?: boolean): Promise<User>;

type R12 = MyReturnType<typeof fetchUser>;
// → Promise<User>
type R13 = MyParameters<typeof fetchUser>;
// → [id: number, withPosts?: boolean | undefined]
type I08 = MyReturnType<() => void>;
// → void

fetchUser は値(関数)なので、型として渡すには typeof を付けます。T extends (...args: any) => any の制約があるので、関数以外を渡すと次のエラーになります。

type R = MyReturnType<string>;
err.ts(3,22): error TS2344: Type 'string' does not satisfy the constraint '(...args: any) => any'.

Awaited を自作する:再帰で Promise をほどく

Conditional Types は自分自身を呼び出せます。Promise の中身を infer で取り出し、まだ Promise ならもう一度ほどく、という再帰にすると Awaited になります。

type Unwrap<T>    = T extends Promise<infer U> ? U : T;                 // 1段だけ
type MyAwaited<T> = T extends PromiseLike<infer U> ? MyAwaited<U> : T;  // 何段でも

type G07 = Unwrap<Promise<Promise<string>>>;
// → Promise<string>
type R14 = MyAwaited<Promise<Promise<string>>>;
// → string
type G05 = MyAwaited<string | Promise<number>>;
// → string | number    ユニオン分配で、メンバーごとにほどかれる
type R15 = MyAwaited<MyReturnType<typeof fetchUser>>;
// → { id: number; name: string; email?: string; readonly createdAt: Date; }

R15 の MyAwaited<MyReturnType<typeof 関数>> は、「非同期関数が最終的に返すデータの型」を取り出す定番の組み合わせです。

タプルやオブジェクトから取り出す

infer は、パターンとして書ける場所ならどこにでも置けます。

type First<T extends readonly unknown[]> =
  T extends readonly [infer H, ...unknown[]] ? H : never;
type Last<T extends readonly unknown[]> =
  T extends readonly [...unknown[], infer L] ? L : never;

type I03 = First<[string, number, boolean]>;  // → string
type I04 = Last<[string, number, boolean]>;   // → boolean

API の結果を表すユニオンから、成功時のデータの型だけを取り出す例です。

type Article = { id: number; title: string; body: string; views: number; published: boolean };
type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string };
type DataOf<R> = R extends { ok: true; data: infer D } ? D : never;

type F03 = DataOf<ApiResult<Article[]>>;
// → Article[]

ここでも分配が働いています。{ ok: false; ... } の側は never になってユニオンから消え、成功側の data の型だけが残ります。

infer を置く位置 取り出せるもの 例
(infer E)[] 配列の要素 ElementOf
(...args: any) => infer R 関数の戻り値 ReturnType
(...args: infer P) => any 関数の引数(タプル) Parameters
PromiseLike<infer U> Promise の中身 Awaited
[infer H, ...unknown[]] タプルの先頭 First
{ data: infer D } プロパティの型 DataOf
`${infer A}_${infer B}` 文字列の一部 SnakeToCamel(後述)

as でキーを付け替える・絞り込む

Mapped Types の in の後ろに as を続けると、回しているキー K を別のキー名に変換できます。 公式ハンドブック(Mapped Types のページ)によると、この機能は TypeScript 4.1 以降で使えます。

{ [K in keyof T as 新しいキー名]: T[K] }

as の右側には型を書きます。ここで never を返すと、そのキーは結果から取り除かれます。

Omit を自作する:never を返してキーを消す

type MyOmit<T, K extends keyof any> = { [P in keyof T as P extends K ? never : P]: T[P] };

type R07 = MyOmit<User, "email" | "createdAt">;
// → { id: number; name: string; }

「キー P が取り除く対象 K に含まれていれば never(消す)、そうでなければ P のまま(残す)」という Conditional Types を as の中に書いています。Mapped Types と Conditional Types を1行で組み合わせた例です。

Omit は、Pick と Exclude の組み合わせでも書けます。組み込みの Omit はこちらの形で定義されています。

type MyOmit2<T, K extends keyof any> = MyPick<T, MyExclude<keyof T, K>>;

type R07b = MyOmit2<User, "email" | "createdAt">;
// → { id: number; name: string; }

as Exclude<K, "body"> のように、as の右側に Exclude を直接書く方法もあります。

type F07 = { [K in keyof Article as Exclude<K, "body">]: Article[K] };
// → { id: number; title: string; views: number; published: boolean; }

値の型でプロパティを絞り込む

as の条件にキーではなく値の型 T[K] を使うと、「値が string のプロパティだけ」といった絞り込みができます。

type PickByValue<T, V> = { [K in keyof T as T[K] extends V ? K : never]: T[K] };
type OmitByValue<T, V> = { [K in keyof T as T[K] extends V ? never : K]: T[K] };

type K03 = PickByValue<Article, string>;
// → { title: string; body: string; }
type K04 = OmitByValue<Article, string>;
// → { id: number; views: number; published: boolean; }

Template Literal Types でキー名を変換する

Template Literal Types は、文字列リテラル型をバッククォートの中で組み立てる構文 です。ユニオンを埋め込むと、すべての組み合わせが作られます。

type K09 = `${"top" | "bottom"}-${"left" | "right"}`;
// → "top-left" | "top-right" | "bottom-left" | "bottom-right"

大文字・小文字を変える組み込みの型が4つあります。

型 働き 例と結果
Uppercase<S> すべて大文字にする Uppercase<"abc"> は "ABC"
Lowercase<S> すべて小文字にする Lowercase<"ABC"> は "abc"
Capitalize<S> 先頭だけ大文字にする Capitalize<"abc"> は "Abc"
Uncapitalize<S> 先頭だけ小文字にする Uncapitalize<"ABC"> は "aBC"

getter・setter のキーを作る

as と組み合わせると、プロパティ名から規則的に新しいキー名を作れます。

type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
type Setters<T> = { [K in keyof T as `set${Capitalize<string & K>}`]: (v: T[K]) => void };

type K01 = Getters<Article>;
// → { getId: () => number; getTitle: () => string; getBody: () => string; getViews: () => number; getPublished: () => boolean; }
type K02 = Setters<Pick<Article, "title" | "views">>;
// → { setTitle: (v: string) => void; setViews: (v: number) => void; }

string との交差型を書く理由

Capitalize<string & K> の string & は飾りではありません。keyof T は string | number | symbol になり得るため、文字列だけを受け取る Capitalize にそのまま渡すとエラーになります。

type GettersNoAmp<T> = { [K in keyof T as `get${Capitalize<K>}`]: () => T[K] };
err.ts(1,60): error TS2344: Type 'K' does not satisfy the constraint 'string'.
  Type 'keyof T' is not assignable to type 'string'.
    Type 'string | number | symbol' is not assignable to type 'string'.
      Type 'number' is not assignable to type 'string'.

string & K と書くと「K のうち文字列であるもの」に絞られます。数値のキーは string & 1 が never になるので、as の規則どおり結果から消えます。

type K14 = Getters<{ 1: string; name: string }>;
// → { getName: () => string; }

snake_case のキーを camelCase に変換する

Template Literal Types の中で infer を使うと、文字列を区切り文字で分解できます。再帰と組み合わせれば、キー名の命名規則をまるごと変換する型になります。

type SnakeToCamel<S extends string> =
  S extends `${infer Head}_${infer Tail}` ? `${Head}${Capitalize<SnakeToCamel<Tail>>}` : S;

type CamelKeys<T> = { [K in keyof T as SnakeToCamel<string & K>]: T[K] };

type Row = { user_id: number; first_name: string; created_at: Date };

type K06 = SnakeToCamel<"created_at_utc">;
// → "createdAtUtc"
type K07 = CamelKeys<Row>;
// → { userId: number; firstName: string; createdAt: Date; }

SnakeToCamel は「最初のアンダースコアで前後に分け、後ろ側を再帰的に変換して先頭を大文字にする」という処理です。

URL のパスからパラメータ名を取り出す型も、同じ考え方で書けます。

type RouteParams<S extends string> =
  S extends `${string}:${infer P}/${infer Rest}` ? P | RouteParams<Rest>
  : S extends `${string}:${infer P}` ? P
  : never;

type K12 = RouteParams<"/users/:userId/posts/:postId">;
// → "userId" | "postId"

コピペ用:キー名を変換する関数に型を付ける

型だけでなく、実行時の値も変換する完成形です。データベースの行(snake_case)を、画面側で使うオブジェクト(camelCase)に変換する場面を想定しています。

// camel.ts
type SnakeToCamel<S extends string> =
  S extends `${infer Head}_${infer Tail}` ? `${Head}${Capitalize<SnakeToCamel<Tail>>}` : S;
type CamelKeys<T> = { [K in keyof T as SnakeToCamel<string & K>]: T[K] };

function toCamelKeys<T extends Record<string, unknown>>(row: T): CamelKeys<T> {
  const out: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(row)) {
    out[key.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())] = value;
  }
  return out as CamelKeys<T>;
}

const row = { user_id: 7, first_name: "Aoi", created_at: "2026-10-03" };
const user = toCamelKeys(row);
console.log(user);
console.log(user.userId, user.firstName, user.createdAt);
$ npx tsc --strict --target es2022 --module commonjs camel.ts && node camel.js
{ userId: 7, firstName: 'Aoi', createdAt: '2026-10-03' }
7 Aoi 2026-10-03

変換後の user に対して、変換前のキー名でアクセスするとコンパイルの段階で止まり、正しいキー名の候補まで表示されます。

declare const user: CamelKeys<{ user_id: number; first_name: string }>;
user.user_id;
camel_err.ts(5,6): error TS2551: Property 'user_id' does not exist on type 'CamelKeys<{ user_id: number; first_name: string; }>'. Did you mean 'userId'?

関数の中の as CamelKeys<T> は、実行時の変換結果が型と一致していることを書き手が約束する型アサーションです。型の変換規則と、実行時の replace の規則をそろえておくことが前提になります。 この例の正規表現はアンダースコアの直後が小文字のときだけ変換するので、数字や大文字が続くキー名を扱う場合は、両方の規則を合わせて見直してください。

そのまま使える応用パターン集

ここまでの部品を組み合わせた、よく使う型です。

ネストした型を再帰で変換する

type DeepReadonly<T> =
  T extends (...a: any[]) => any ? T
  : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

type Cfg = { db: { host: string; port: number }; tags: string[] };

type M01 = DeepReadonly<Cfg>;
// → { readonly db: { readonly host: string; readonly port: number; }; readonly tags: readonly string[]; }

組み込みの Readonly や Partial は1階層めだけに効きます。内側まで変換したいときは、このように「オブジェクトなら自分自身をもう一度当てる」再帰にします。

一部のキーだけ省略可能にして、表示も整える

type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type Simplify<T> = { [K in keyof T]: T[K] } & {};

type M10 = PartialBy<Article, "views" | "published">;
// → Omit<Article, "published" | "views"> & Partial<Pick<Article, "published" | "views">>
type M11 = Simplify<PartialBy<Article, "views" | "published">>;
// → { id: number; title: string; body: string; published?: boolean | undefined; views?: number | undefined; }

M10 のように、交差型はホバーしても中身が展開されず読みにくいことがあります。Mapped Types で1回写し直す Simplify を通すと、M11 のように展開された形で表示されます。 型の意味は変わらず、見え方だけが整います。

つまずきやすい箇所と実際のエラー文

ユニオンに Omit を使うと共通のキーだけが残る

type M13 = Omit<{ a: 1; b: 2 } | { a: 3; c: 4 }, "a">;
// → {}

type DistOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
type M14 = DistOmit<{ a: 1; b: 2 } | { a: 3; c: 4 }, "a">;
// → Omit<{ a: 1; b: 2; }, "a"> | Omit<{ a: 3; c: 4; }, "a">

組み込みの Omit は内部で keyof を使うため、ユニオンを渡すと共通のキー(ここでは a だけ)を基準に処理され、b も c も残りません。メンバーごとに Omit したいときは、T extends any ? ... : never で分配させます。

エラー番号から直し方を引く

TS2540 は readonly を付けた自作の型のプロパティに代入したとき、TS2741 は Getters で作った型に getName のないオブジェクトを代入したときに、tsc 5.9.3 で出ることを確認しました。

エラー番号 出る場面 直し方
TS2344 型引数が制約を満たしていない(Capitalize に keyof T をそのまま渡す、ReturnType に関数以外を渡す) string & K で絞る、渡す型を見直す
TS2322(定義時) in の右側がキーになれない型 型引数に extends keyof T や extends keyof any を付ける
TS2536 T[P] の P が T のキーと限らない P の取り得る範囲を keyof T に制約する
TS2322(代入時) 値の型が変換後の型と合わない 値を直すか、変換の定義を見直す
TS2540 readonly を付けたプロパティへの代入 書き換えが必要なら Mutable で外した型を使う
TS2741 as で作ったキーが不足している 表示されたキー名を追加する
TS2551 変換前のキー名でアクセスした 候補として表示されたキー名に直す

思った結果にならないときのチェック表

症状 原因 対処
結果が never になる never を渡して分配された [T] extends [never] で先に判定する
"yes" と "no" の両方が返る ユニオン・boolean・any が分配された 1回だけ判定したいならタプルで包む
? や readonly が消えた in の右側にキー名を直接並べた keyof T を使う形に直す
ユニオンに Omit を使ったらキーが減りすぎた keyof がユニオンの共通キーだけを返した T extends any ? Omit<T, K> : never にする
数値のキーが結果から消えた string & K が never になった 仕様どおりの動作。`${K & (string | number)}` と書くと数値のキー 1 も get1 として残る
ホバーしても中身が展開されない 交差型や型の別名がそのまま表示されている Simplify を通す

Mapped Types と Conditional Types の使い分け

やりたいこと 使う構文 代表的な書き方
すべてのプロパティの値の型を変える Mapped Types { [K in keyof T]: 新しい型 }
省略可能・読み取り専用を付け外しする Mapped Types と修飾子 [K in keyof T]-?:
キー名を変える・キーを減らす Mapped Types と as [K in keyof T as 新しいキー]
ユニオンからメンバーを絞り込む Conditional Types(分配) T extends U ? T : never
型の一部を取り出す Conditional Types と infer T extends Promise<infer U> ? U : T
文字列の型を組み立てる・分解する Template Literal Types `get${Capitalize<S>}`
ネストした型を内側まで変換する 両方と再帰 DeepReadonly

「オブジェクトの形を変えるなら Mapped Types、型を選ぶ・取り出すなら Conditional Types」 と覚えておくと、どちらから書き始めるかを決めやすくなります。組み込みのユーティリティ型で足りる場面ではそちらを使い、足りないときにこの記事の部品で自作する、という順番がおすすめです。

よくある質問

Q. Mapped Types と Record は何が違いますか? A. Record は Mapped Types で作られたユーティリティ型の1つで、定義は { [P in K]: V } です。すべてのキーに同じ値の型 V を割り当てます。Mapped Types を直接書くと、T[K] のようにキーごとに異なる値の型を割り当てたり、as でキー名を変えたりできます。

Q. extends が2種類あるように見えます。どう見分けますか? A. 型引数の宣言の中(山かっこの中)にあれば制約、? と : を伴っていれば条件分岐です。type MyPick<T, K extends keyof T> は制約、T extends string ? "yes" : "no" は条件分岐です。ReturnType の自作のように、1つの定義に両方が出てくることもあります。

Q. infer はどこでも使えますか? A. Conditional Types の extends の右側だけで使えます。取り出した型は真の側で参照できます。偽の側では参照できないので、取り出せなかったときの結果(never や元の型)を偽の側に書きます。

まとめ

  • Mapped Types は { [K in keyof T]: T[K] } を基本形に、キーを1つずつ回して新しいオブジェクト型を作る構文 です。?・readonly はそのまま書けば付き、先頭に - を付ければ外れます。
  • Conditional Types は T extends U ? X : Y で型を分岐する構文 です。never を返すとユニオンからメンバーを取り除けます。
  • 型引数をそのまま extends の左側に置くと、ユニオンはメンバーごとに判定されます(ユニオン分配)。 boolean と any も分かれて判定されます。
  • never を分配に渡すと結果は never になります。 never の判定や、ユニオン全体での判定には [T] extends [U] を使います。
  • infer は、マッチしたパターンの一部を取り出すキーワード です。関数の戻り値、Promise の中身、タプルの要素、文字列の一部まで取り出せます。
  • as はキー名の付け替えと絞り込みに使います。 Template Literal Types と組み合わせると、getter 名や camelCase への変換が型だけで書けます。
  • Partial・Pick・Omit・ReturnType・Awaited は、すべてこの部品の組み合わせで自作できます。 仕組みがわかると、組み込みの型で足りない場面でも自分で型を組み立てられます。

参考

  • TypeScript Handbook「Conditional Types」(typescriptlang.org/docs/handbook/2/conditional-types.html)
  • TypeScript Handbook「Mapped Types」(typescriptlang.org/docs/handbook/2/mapped-types.html)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「TypeScript Mapped/Conditional Types早見表」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?