型を「計算・変換・生成」する技術
型を書くのではなく、型を作る。それが中級から上級への境界線だ。
この記事で得られること
- Conditional Types・Mapped Types・Template Literal Typesを自在に使える
- 共変・反変・不変の仕組みを理解し、型エラーの原因を正確に把握できる
- 型レベルのプログラミングで、ライブラリ級の型を設計できる
Chapter 1: Conditional Types 完全攻略
1-1. 基本構文
Conditional Typesは「型の三項演算子」です。
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true("hello" は string を extends する)
T extends U ? X : Y という構文で、「TがUに代入可能なら X、そうでなければ Y」を返します。
1-2. infer:型を「分解」して取り出す
infer はConditional Typesの中で使える「型変数の宣言」です。
// 関数の戻り値の型を取り出す
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function fetchUser(): Promise<User> { /* ... */ }
type Result = ReturnType<typeof fetchUser>; // Promise<User>
// Promiseを unwrap する
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type Resolved = Awaited<Promise<Promise<string>>>; // string
// 配列の要素型を取り出す
type ElementType<T> = T extends (infer E)[] ? E : never;
type Elem = ElementType<string[]>; // string
// 関数の第一引数の型を取り出す
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type F = FirstArg<(a: string, b: number) => void>; // string
1-3. 分配的Conditional Types
Union型にConditional Typesを適用すると、各メンバーに分配されます。
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
// string extends any ? string[] : never
// | number extends any ? number[] : never
// => string[] | number[]
// 分配を防ぎたい場合はタプルでラップする
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type B = ToArrayNonDist<string | number>; // (string | number)[]
1-4. 実用例:Conditional Typesで型ユーティリティを作る
// null と undefined を再帰的に除去する
type DeepNonNullable<T> = T extends null | undefined
? never
: T extends object
? { [K in keyof T]: DeepNonNullable<T[K]> }
: T;
// オブジェクトの値が特定の型のキーだけを取り出す
type KeysOfType<T, ValueType> = {
[K in keyof T]: T[K] extends ValueType ? K : never;
}[keyof T];
interface User {
id: string;
name: string;
age: number;
isActive: boolean;
}
type StringKeys = KeysOfType<User, string>; // "id" | "name"
type NumberKeys = KeysOfType<User, number>; // "age"
1-5. 再帰的Conditional Types(TS 4.1+)
// 深いネストのオブジェクトを全てReadonlyにする
type DeepReadonly<T> = T extends (infer E)[]
? ReadonlyArray<DeepReadonly<E>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// ネストされたオブジェクトのパスを型として表現する
type Paths<T, Prefix extends string = ""> = {
[K in keyof T & string]: T[K] extends object
? Paths<T[K], `${Prefix}${K}.`> | `${Prefix}${K}`
: `${Prefix}${K}`;
}[keyof T & string];
type Config = { db: { host: string; port: number }; timeout: number };
type ConfigPaths = Paths<Config>; // "db" | "db.host" | "db.port" | "timeout"
Chapter 2: Mapped Types と keyof の深掘り
2-1. Mapped Typesの基本
Mapped Typesは「既存の型の全プロパティに変換を適用する」型操作です。
// 基本構文
type Mapped<T> = {
[K in keyof T]: T[K]; // T の全プロパティをそのままコピー
};
// 組み込みUtility Typesの実装を読み解く
type Readonly<T> = {
readonly [K in keyof T]: T[K]; // 全プロパティに readonly を付与
};
type Partial<T> = {
[K in keyof T]?: T[K]; // 全プロパティをオプショナルに
};
type Required<T> = {
[K in keyof T]-?: T[K]; // オプショナルを除去(- で修飾子を取り除く)
};
2-2. Key Remapping(TS 4.1+)
as を使ってキー名を変換できます。
// 全プロパティ名に "get" プレフィックスを付けた Getter を生成
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User { name: string; age: number; }
type UserGetters = Getters<User>;
// => { getName: () => string; getAge: () => number }
// 特定のプロパティを除外する
type OmitPrivate<T> = {
[K in keyof T as K extends `_${string}` ? never : K]: T[K];
};
interface Config {
host: string;
_password: string; // プライベートプロパティ(慣習)
port: number;
}
type PublicConfig = OmitPrivate<Config>;
// => { host: string; port: number }
2-3. keyof の完全理解
interface User { id: string; name: string; age: number; }
type UserKeys = keyof User; // "id" | "name" | "age"
// インデックス型のkeyof
type Dict = { [key: string]: number };
type DictKeys = keyof Dict; // string | number
// ※ JSでは obj[0] と obj["0"] は同じキーを指すため number も含まれる
// typeof と組み合わせる
const config = { host: "localhost", port: 3000 } as const;
type ConfigKeys = keyof typeof config; // "host" | "port"
type ConfigValues = typeof config[keyof typeof config]; // "localhost" | 3000
2-4. Mapped Types の実践例
// イベントハンドラの型を自動生成
type EventHandlers<T extends string> = {
[K in T as `on${Capitalize<K>}`]?: (event: Event) => void;
};
type ClickEvents = EventHandlers<"click" | "dblclick" | "contextmenu">;
// => { onClick?: ...; onDblclick?: ...; onContextmenu?: ... }
// バリデーションルールを型から生成
type ValidationRules<T> = {
[K in keyof T]?: {
required?: boolean;
minLength?: T[K] extends string ? number : never;
min?: T[K] extends number ? number : never;
max?: T[K] extends number ? number : never;
};
};
type UserValidation = ValidationRules<User>;
// name の minLength は有効、age の min/max は有効
// name の min は never(型が合わない)
Chapter 3: Template Literal Types 実践
3-1. 基本構文
type Greeting = `Hello, ${string}!`; // "Hello, xxx!" の形を持つ全ての string
// Union型と組み合わせると爆発的に展開される
type Color = "red" | "green" | "blue";
type Size = "sm" | "md" | "lg";
type ClassName = `${Color}-${Size}`;
// => "red-sm" | "red-md" | "red-lg" | "green-sm" | ... (9種類)
3-2. 組み込みの文字列操作型
type S = "hello world";
type U = Uppercase<S>; // "HELLO WORLD"
type L = Lowercase<S>; // "hello world"
type C = Capitalize<S>; // "Hello world"
type U2 = Uncapitalize<"HelloWorld">; // "helloWorld"
3-3. 実践:型安全なイベントシステム
type EventMap = {
click: { x: number; y: number };
keydown: { key: string; code: string };
resize: { width: number; height: number };
};
type EventListener<T extends keyof EventMap> =
(event: EventMap[T]) => void;
// on<EventName> の形でハンドラを定義する
type EventHandlerMap = {
[K in keyof EventMap as `on${Capitalize<K>}`]: EventListener<K>;
};
// => { onClick: (e: { x: number; y: number }) => void; ... }
3-4. 実践:APIルートの型安全化
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
type ApiVersion = "v1" | "v2";
type Resource = "users" | "posts" | "comments";
type ResourceId = `${Resource}/:id`;
type ApiRoute =
| `/${ApiVersion}/${Resource}`
| `/${ApiVersion}/${ResourceId}`;
// リクエスト関数の型付け
declare function apiRequest(
method: HttpMethod,
route: ApiRoute
): Promise<unknown>;
apiRequest("GET", "/v1/users"); // ✅
apiRequest("GET", "/v1/users/:id"); // ✅
apiRequest("GET", "/v3/users"); // ❌ コンパイルエラー
apiRequest("CONNECT", "/v1/users"); // ❌ コンパイルエラー
3-5. 実践:CSSクラス名の型安全化
type Breakpoint = "sm" | "md" | "lg" | "xl";
type Property = "p" | "m" | "px" | "py" | "mx" | "my";
type Scale = "0" | "1" | "2" | "4" | "8" | "16";
type SpacingClass = `${Property}-${Scale}`;
type ResponsiveClass = `${Breakpoint}:${SpacingClass}`;
type TailwindLike = SpacingClass | ResponsiveClass;
function cn(...classes: TailwindLike[]): string {
return classes.join(" ");
}
cn("p-4", "md:px-8"); // ✅
cn("p-3"); // ❌ Scale に "3" はない
Chapter 4: 共変・反変・不変を理解する
TypeScriptで最も難解なトピックの1つです。型エラーのメッセージが理解できない原因の多くはここにあります。
4-1. 変性(Variance)とは何か
「変性」は「複合型(配列・関数など)の互換性が、その要素型の互換性とどう対応するか」を表す概念です。
まず基本となる型の継承関係を定義します:
class Animal { breathe() {} }
class Dog extends Animal { bark() {} }
class Poodle extends Dog { dance() {} }
// Poodle は Dog のサブタイプ
// Dog は Animal のサブタイプ
4-2. 共変(Covariance):サブタイプ関係が「保たれる」
配列・オブジェクトのプロパティ(読み取り専用)は共変です。
// Dog は Animal のサブタイプ
// → Dog[] は Animal[] のサブタイプ(共変)
const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs; // ✅ 共変なので OK
// 読み取り専用プロパティも共変
interface Box<T> { readonly value: T }
const dogBox: Box<Dog> = { value: new Dog() };
const animalBox: Box<Animal> = dogBox; // ✅ OK
4-3. 反変(Contravariance):サブタイプ関係が「逆転する」
関数の引数は反変です。
type Handler<T> = (val: T) => void;
// Dog は Animal のサブタイプ
// しかし Handler<Dog> と Handler<Animal> の関係は「逆」
const handleAnimal: Handler<Animal> = (a) => a.breathe();
const handleDog: Handler<Dog> = handleAnimal; // ✅ OK
// なぜ? handleAnimal は任意の Animal を受け取れる = Dog も受け取れる
const handleDog2: Handler<Dog> = (d) => d.bark();
const handleAnimal2: Handler<Animal> = handleDog2; // ❌ エラー
// なぜ? handleDog2 は Dog.bark() を呼ぶが、Animal には bark がない
直感的に言うと:「より広い型を受け取れる関数は、より狭い型を受け取れる関数の代わりになれる」
4-4. 不変(Invariance):どちらの関係も成立しない
読み書き両方可能な型パラメータは不変になります。
// 書き込み可能な配列は不変
const dogs: Dog[] = [new Dog()];
// const animals: Animal[] = dogs; // 実はこれは安全でない!
// なぜか?
animals.push(new Animal()); // Animal を追加
dogs[0].bark(); // dogs は Animal になっているのに bark を呼ぶ → 危険
// TypeScriptは配列を共変として扱う(利便性のための妥協)
// より厳密にしたい場合は ReadonlyArray を使う
4-5. strictFunctionTypes と関数の変性
strictFunctionTypes: true(strict: true に含まれる)を有効にすると、関数の引数が正しく反変チェックされます。
// strictFunctionTypes が有効な場合
type Callback<T> = (val: T) => void;
declare let cbDog: Callback<Dog>;
declare let cbAnimal: Callback<Animal>;
cbDog = cbAnimal; // ✅ OK(反変:より広い型を受け取れる方向)
cbAnimal = cbDog; // ❌ エラー(反変:逆方向は不可)
4-6. 変性の実務的影響
// ジェネリクスの変性を意識した設計
// Producer(生産者):T を返すだけ → 共変として安全
interface Producer<out T> { // TS 4.7+ の変性アノテーション
produce(): T;
}
// Consumer(消費者):T を受け取るだけ → 反変として安全
interface Consumer<in T> {
consume(val: T): void;
}
// Transformer(変換者):T を受け取り T を返す → 不変
interface Transformer<T> {
transform(val: T): T;
}
まとめ:Vol.2 のチェックリスト
- [ ]
inferを使って型を分解・取り出せる - [ ] 分配的Conditional Typesと非分配の違いを説明できる
- [ ] Key Remapping で型のキー名を変換できる
- [ ]
keyofがstring | numberを返す場合があることを知っている - [ ] Template Literal Types でAPIルートを型安全に定義できる
- [ ] 共変・反変・不変の違いを具体的なコードで説明できる
- [ ]
strictFunctionTypesが何をチェックするか理解している
次回 Vol.3「実践設計パターン」 では、Branded Types・エラーハンドリング・zod・DI・State Machineを解説します。
質問・フィードバックはコメントにてどうぞ。