Web Development
TypeScript高度な型システム完全ガイド - ジェネリック、条件付き型、型ガード徹底解説
kitahara-dev•2025/10/30•13 min read
TypeScript高度な型システム完全ガイド
TypeScriptの型システムは、JavaScriptに静的型付けをもたらす強力なツールです。このガイドでは、初級者から中級者が理解すべき高度な型定義テクニックを、実践的なコード例とともに徹底解説します。
📋 目次
- ジェネリック型の基礎と応用
- 条件付き型とinferキーワード
- マップされた型とキー再マッピング
- ユーティリティ型の完全ガイド
- テンプレートリテラル型
- 型ガードとユーザー定義型ガード
- 関数オーバーロード
- as const アサーション
- 判別可能なユニオン型
- 再帰的型
- 型レベルプログラミング
- 実践的な型パターン
1. ジェネリック型の基礎と応用
基本的なジェネリック
型パラメータを使用して、型安全で再利用可能なコードを書けます。
// ❌ 悪い例:any使用で型安全性を失う
interface BadContainer {
value: any;
getValue(): any;
}
// ✅ 良い例:ジェネリックで型安全性を保つ
interface Container<T> {
value: T;
getValue(): T;
setValue(value: T): void;
}
const stringContainer: Container<string> = {
value: 'hello',
getValue() { return this.value; },
setValue(value) { this.value = value; },
};
const numberContainer: Container<number> = {
value: 42,
getValue() { return this.value; },
setValue(value) { this.value = value; },
};
ジェネリック制約
型パラメータに制約を追加して、特定の構造を持つ型のみを受け入れます。
// lengthプロパティを持つ型のみ受け入れる
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength('hello'); // ✅ OK: string has length
logLength([1, 2, 3]); // ✅ OK: array has length
logLength({ length: 10 }); // ✅ OK: object has length
// logLength(42); // ❌ Error: number doesn't have length
// 複数の型パラメータと制約
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const merged = merge(
{ name: 'John' },
{ age: 30 }
);
// merged: { name: string } & { age: number }
ジェネリックのデフォルト型
// デフォルト型パラメータ
interface Response<T = unknown> {
data: T;
status: number;
message: string;
}
const response1: Response<User> = {
data: { id: 1, name: 'John' },
status: 200,
message: 'Success',
};
const response2: Response = {
data: { anything: 'goes' }, // unknown型
status: 200,
message: 'Success',
};
ジェネリッククラスとメソッド
class DataStore<T> {
private data: T[] = [];
add(item: T): void {
this.data.push(item);
}
get(index: number): T | undefined {
return this.data[index];
}
// ジェネリックメソッド
find<K extends keyof T>(key: K, value: T[K]): T | undefined {
return this.data.find(item => item[key] === value);
}
}
interface User {
id: number;
name: string;
}
const userStore = new DataStore<User>();
userStore.add({ id: 1, name: 'Alice' });
userStore.add({ id: 2, name: 'Bob' });
const user = userStore.find('name', 'Alice'); // User | undefined
2. 条件付き型とinferキーワード
基本的な条件付き型
型レベルでif-else文のようなロジックを記述できます。
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<number>; // false
// 実用例:配列型を抽出
type IsArray<T> = T extends any[] ? true : false;
type C = IsArray<string[]>; // true
type D = IsArray<string>; // false
inferキーワード
条件付き型の中で型を推論して抽出します。
// 関数の戻り値型を抽出
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: 'John' };
}
type User = ReturnType<typeof getUser>; // { id: number; name: string }
// 配列の要素型を抽出
type ElementType<T> = T extends (infer E)[] ? E : never;
type StringArray = ElementType<string[]>; // string
type NumberArray = ElementType<number[]>; // number
// Promise型の解決型を抽出
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type PromiseString = UnwrapPromise<Promise<string>>; // string
type PlainString = UnwrapPromise<string>; // string
// 関数の第一引数型を抽出
type FirstArg<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
function exampleFunc(str: string, num: number) {
return str + num;
}
type FirstParam = FirstArg<typeof exampleFunc>; // string
ネストしたinfer
// ネストしたPromiseをフラット化
type DeepUnwrapPromise<T> = T extends Promise<infer U>
? DeepUnwrapPromise<U>
: T;
type NestedPromise = Promise<Promise<Promise<string>>>;
type Unwrapped = DeepUnwrapPromise<NestedPromise>; // string
3. マップされた型とキー再マッピング
基本的なマップされた型
既存の型から新しい型を生成します。
// 全プロパティをreadonlyに
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// 全プロパティをオプショナルに
type Partial<T> = {
[K in keyof T]?: T[K];
};
interface User {
id: number;
name: string;
email: string;
}
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly email: string }
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string }
キー再マッピング(TypeScript 4.1+)
// プロパティ名を変換
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// }
// 特定のキーを除外
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K];
};
interface Mixed {
name: string;
age: number;
isActive: boolean;
count: number;
}
type OnlyStrings = OmitByType<Mixed, number>;
// { name: string; isActive: boolean }
テンプレートリテラル型でのキー生成
// イベントハンドラー型を自動生成
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void;
};
interface FormData {
username: string;
email: string;
age: number;
}
type FormHandlers = EventHandlers<FormData>;
// {
// onUsernameChange: (value: string) => void;
// onEmailChange: (value: string) => void;
// onAgeChange: (value: number) => void;
// }
4. ユーティリティ型の完全ガイド
TypeScriptには多数の組み込みユーティリティ型があります。
基本的なユーティリティ型
interface User {
id: number;
name: string;
email: string;
age?: number;
}
// Partial<T> - 全プロパティをオプショナルに
type PartialUser = Partial<User>;
// Required<T> - 全プロパティを必須に
type RequiredUser = Required<User>;
// Readonly<T> - 全プロパティを読み取り専用に
type ReadonlyUser = Readonly<User>;
// Pick<T, K> - 特定プロパティのみ選択
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string }
// Omit<T, K> - 特定プロパティを除外
type UserWithoutEmail = Omit<User, 'email'>;
// { id: number; name: string; age?: number }
// Record<K, T> - キーと値の型を指定してオブジェクト型を生成
type UserRoles = Record<'admin' | 'user' | 'guest', boolean>;
// { admin: boolean; user: boolean; guest: boolean }
高度なユーティリティ型
// Exclude<T, U> - Uに割り当て可能な型をTから除外
type T1 = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
type T2 = Exclude<string | number | (() => void), Function>; // string | number
// Extract<T, U> - Uに割り当て可能な型のみをTから抽出
type T3 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'
type T4 = Extract<string | number | (() => void), Function>; // () => void
// NonNullable<T> - nullとundefinedを除外
type T5 = NonNullable<string | number | undefined | null>; // string | number
// ReturnType<T> - 関数の戻り値型を取得
function getUser() {
return { id: 1, name: 'John' };
}
type UserType = ReturnType<typeof getUser>; // { id: number; name: string }
// Parameters<T> - 関数のパラメータ型をタプルとして取得
function createUser(name: string, age: number) {
return { name, age };
}
type CreateUserParams = Parameters<typeof createUser>; // [string, number]
// InstanceType<T> - クラスのインスタンス型を取得
class User {
constructor(public name: string) {}
}
type UserInstance = InstanceType<typeof User>; // User
カスタムユーティリティ型の作成
// ネストしたPartial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
}
type PartialConfig = DeepPartial<Config>;
// すべてのネストレベルでオプショナル
// 読み取り専用の配列とオブジェクト
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// nullableなプロパティのみ選択
type NullableKeys<T> = {
[K in keyof T]: null extends T[K] ? K : never;
}[keyof T];
interface Data {
id: number;
name: string | null;
email: string | null;
age: number;
}
type NullableDataKeys = NullableKeys<Data>; // 'name' | 'email'
5. テンプレートリテラル型
基本的なテンプレートリテラル型
文字列リテラル型を組み合わせて新しい型を生成します。
type EventName = `on${Capitalize<'click' | 'hover' | 'focus'>}`;
// 'onClick' | 'onHover' | 'onFocus'
type Direction = 'top' | 'right' | 'bottom' | 'left';
type Margin = `margin-${Direction}`;
// 'margin-top' | 'margin-right' | 'margin-bottom' | 'margin-left'
// 複数の型を組み合わせ
type Size = 'small' | 'medium' | 'large';
type Color = 'red' | 'blue' | 'green';
type ButtonClass = `btn-${Size}-${Color}`;
// 'btn-small-red' | 'btn-small-blue' | ... (9種類の組み合わせ)
実用的なテンプレートリテラル型
// CSSプロパティ型の生成
type CSSProperty =
| `padding-${'top' | 'right' | 'bottom' | 'left'}`
| `margin-${'top' | 'right' | 'bottom' | 'left'}`;
const style: Record<CSSProperty, string> = {
'padding-top': '10px',
'padding-right': '10px',
'padding-bottom': '10px',
'padding-left': '10px',
'margin-top': '20px',
'margin-right': '20px',
'margin-bottom': '20px',
'margin-left': '20px',
};
// APIエンドポイント型
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/posts' | '/comments';
type APIRoute = `${HTTPMethod} ${Endpoint}`;
function callAPI(route: APIRoute) {
// 'GET /users' | 'POST /users' | ...
}
callAPI('GET /users'); // ✅ OK
// callAPI('GET /invalid'); // ❌ Error
組み込み文字列操作型
// Uppercase, Lowercase, Capitalize, Uncapitalize
type UpperGreeting = Uppercase<'hello'>; // 'HELLO'
type LowerGreeting = Lowercase<'HELLO'>; // 'hello'
type CapitalizedGreeting = Capitalize<'hello'>; // 'Hello'
type UncapitalizedGreeting = Uncapitalize<'Hello'>; // 'hello'
// 実用例:イベントハンドラー
type EventKeys = 'click' | 'mouseenter' | 'mouseleave';
type EventHandlerNames = `on${Capitalize<EventKeys>}`;
// 'onClick' | 'onMouseenter' | 'onMouseleave'
6. 型ガードとユーザー定義型ガード
組み込み型ガード
function processValue(value: string | number) {
// typeof型ガード
if (typeof value === 'string') {
return value.toUpperCase(); // value: string
} else {
return value.toFixed(2); // value: number
}
}
function getLength(value: string | any[]) {
// Array.isArray型ガード
if (Array.isArray(value)) {
return value.length; // value: any[]
} else {
return value.length; // value: string
}
}
class Dog {
bark() { console.log('Woof!'); }
}
class Cat {
meow() { console.log('Meow!'); }
}
function makeSound(animal: Dog | Cat) {
// instanceof型ガード
if (animal instanceof Dog) {
animal.bark(); // animal: Dog
} else {
animal.meow(); // animal: Cat
}
}
ユーザー定義型ガード
interface User {
type: 'user';
name: string;
email: string;
}
interface Admin {
type: 'admin';
name: string;
permissions: string[];
}
// ユーザー定義型ガード
function isAdmin(person: User | Admin): person is Admin {
return person.type === 'admin';
}
function greet(person: User | Admin) {
if (isAdmin(person)) {
console.log(`Admin ${person.name} has ${person.permissions.length} permissions`);
// person: Admin
} else {
console.log(`User ${person.name}`);
// person: User
}
}
// 汎用的な型ガード
function isNonNull<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
const values = [1, null, 2, undefined, 3];
const nonNullValues = values.filter(isNonNull); // number[]
asserts型ガード(TypeScript 3.7+)
// assertsキーワードで型を断言
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value must be a string');
}
}
function processInput(input: unknown) {
assertIsString(input);
// この時点でinputはstring型
return input.toUpperCase();
}
// nullチェック
function assertNonNull<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error('Value is null or undefined');
}
}
function getUser(id: number): User | null {
return { id, name: 'John', email: 'john@example.com' };
}
const user = getUser(1);
assertNonNull(user);
console.log(user.name); // user: User
7. 関数オーバーロード
同じ関数名で異なるパラメータと戻り値の型を定義します。
// ❌ 悪い例:ユニオン型で曖昧
function badFormat(value: string | number): string | number {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toFixed(2);
}
const result1 = badFormat('hello'); // string | number (曖昧)
// ✅ 良い例:関数オーバーロードで明確
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toFixed(2);
}
const result2 = format('hello'); // string (明確)
const result3 = format(42.567); // string (明確)
// 実用例:createElement関数
function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: 'span'): HTMLSpanElement;
function createElement(tag: 'button'): HTMLButtonElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}
const div = createElement('div'); // HTMLDivElement
const span = createElement('span'); // HTMLSpanElement
const button = createElement('button'); // HTMLButtonElement
8. as const アサーション
値を可能な限り厳密な型として推論します。
// ❌ 通常の推論:広い型
const colors1 = ['red', 'green', 'blue'];
// colors1: string[]
// ✅ as const:厳密な型
const colors2 = ['red', 'green', 'blue'] as const;
// colors2: readonly ['red', 'green', 'blue']
type Color = typeof colors2[number];
// Color: 'red' | 'green' | 'blue'
// オブジェクトでの使用
const config1 = {
apiUrl: 'https://api.example.com',
timeout: 5000,
};
// config1.apiUrl: string
// config1.timeout: number
const config2 = {
apiUrl: 'https://api.example.com',
timeout: 5000,
} as const;
// config2.apiUrl: 'https://api.example.com'
// config2.timeout: 5000
// 実用例:定数定義
const HTTP_STATUS = {
OK: 200,
NOT_FOUND: 404,
INTERNAL_ERROR: 500,
} as const;
type HTTPStatus = typeof HTTP_STATUS[keyof typeof HTTP_STATUS];
// HTTPStatus: 200 | 404 | 500
function handleStatus(status: HTTPStatus) {
// ...
}
handleStatus(HTTP_STATUS.OK); // ✅ OK
// handleStatus(201); // ❌ Error
9. 判別可能なユニオン型
タグ付きユニオン(Discriminated Unions)とも呼ばれます。
// 共通のtypeプロパティで判別
interface SuccessResponse {
type: 'success';
data: {
id: number;
name: string;
};
}
interface ErrorResponse {
type: 'error';
error: {
code: string;
message: string;
};
}
interface LoadingResponse {
type: 'loading';
}
type APIResponse = SuccessResponse | ErrorResponse | LoadingResponse;
function handleResponse(response: APIResponse) {
switch (response.type) {
case 'success':
console.log(response.data.name); // response: SuccessResponse
break;
case 'error':
console.error(response.error.message); // response: ErrorResponse
break;
case 'loading':
console.log('Loading...'); // response: LoadingResponse
break;
}
}
// 実用例:Reduxアクション
type Action =
| { type: 'ADD_TODO'; payload: { text: string } }
| { type: 'REMOVE_TODO'; payload: { id: number } }
| { type: 'TOGGLE_TODO'; payload: { id: number } };
function reducer(state: any, action: Action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { text: action.payload.text }];
case 'REMOVE_TODO':
return state.filter((todo: any) => todo.id !== action.payload.id);
case 'TOGGLE_TODO':
return state.map((todo: any) =>
todo.id === action.payload.id ? { ...todo, done: !todo.done } : todo
);
}
}
10. 再帰的型
型定義の中で自分自身を参照します。
// JSONの型定義
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
const jsonData: JSONValue = {
name: 'John',
age: 30,
hobbies: ['reading', 'coding'],
address: {
city: 'Tokyo',
coords: {
lat: 35.6762,
lng: 139.6503,
},
},
};
// ツリー構造の型定義
interface TreeNode<T> {
value: T;
children: TreeNode<T>[];
}
const fileTree: TreeNode<string> = {
value: 'root',
children: [
{
value: 'src',
children: [
{ value: 'index.ts', children: [] },
{ value: 'utils.ts', children: [] },
],
},
{
value: 'tests',
children: [
{ value: 'test.ts', children: [] },
],
},
],
};
// パス型の生成(ネストしたオブジェクトのキーパス)
type PathsToStringProps<T> = T extends string
? []
: {
[K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>];
}[Extract<keyof T, string>];
type Join<T extends string[], D extends string = '.'> = T extends []
? never
: T extends [infer F]
? F
: T extends [infer F, ...infer R]
? F extends string
? `${F}${D}${Join<Extract<R, string[]>, D>}`
: never
: string;
interface NestedObj {
user: {
profile: {
name: string;
age: number;
};
settings: {
theme: string;
};
};
}
type Paths = Join<PathsToStringProps<NestedObj>>;
// 'user.profile.name' | 'user.profile.age' | 'user.settings.theme'
11. 型レベルプログラミング
型レベルでの条件分岐
// Promiseをフラット化する型
type Awaited<T> = T extends Promise<infer U>
? Awaited<U>
: T;
type T1 = Awaited<Promise<string>>; // string
type T2 = Awaited<Promise<Promise<number>>>; // number
// 配列をフラット化する型
type Flatten<T> = T extends any[]
? T[number] extends infer U
? U extends any[]
? Flatten<U>
: U
: never
: T;
type T3 = Flatten<number[]>; // number
type T4 = Flatten<number[][]>; // number
type T5 = Flatten<number[][][]>; // number
型レベルでの演算
// タプルの長さを取得
type Length<T extends any[]> = T['length'];
type L1 = Length<[1, 2, 3]>; // 3
type L2 = Length<[]>; // 0
// タプルの最初の要素を取得
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type F1 = First<[1, 2, 3]>; // 1
type F2 = First<['a', 'b', 'c']>; // 'a'
// タプルの最後の要素を取得
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type L3 = Last<[1, 2, 3]>; // 3
type L4 = Last<['a', 'b', 'c']>; // 'c'
12. 実践的な型パターン
Builderパターン
class QueryBuilder<T = {}> {
private query: T;
constructor(query: T = {} as T) {
this.query = query;
}
where<K extends string, V>(key: K, value: V): QueryBuilder<T & Record<K, V>> {
return new QueryBuilder({ ...this.query, [key]: value });
}
build(): T {
return this.query;
}
}
const query = new QueryBuilder()
.where('name', 'John')
.where('age', 30)
.where('email', 'john@example.com')
.build();
// query: { name: string; age: number; email: string }
型安全なイベントエミッター
type EventMap = {
'user:login': { userId: number; timestamp: Date };
'user:logout': { userId: number };
'data:update': { key: string; value: any };
};
class TypedEventEmitter<T extends Record<string, any>> {
private listeners: {
[K in keyof T]?: Array<(data: T[K]) => void>;
} = {};
on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit<K extends keyof T>(event: K, data: T[K]): void {
const eventListeners = this.listeners[event];
if (eventListeners) {
eventListeners.forEach(listener => listener(data));
}
}
}
const emitter = new TypedEventEmitter<EventMap>();
emitter.on('user:login', (data) => {
console.log(`User ${data.userId} logged in at ${data.timestamp}`);
// data: { userId: number; timestamp: Date }
});
emitter.emit('user:login', { userId: 1, timestamp: new Date() }); // ✅ OK
// emitter.emit('user:login', { userId: '1' }); // ❌ Error: wrong type
型安全なフォームハンドリング
type FormField<T> = {
value: T;
error?: string;
touched: boolean;
};
type FormState<T> = {
[K in keyof T]: FormField<T[K]>;
};
interface UserForm {
username: string;
email: string;
age: number;
}
type UserFormState = FormState<UserForm>;
// {
// username: FormField<string>;
// email: FormField<string>;
// age: FormField<number>;
// }
const formState: UserFormState = {
username: { value: '', touched: false },
email: { value: '', touched: false },
age: { value: 0, touched: false },
};
📝 まとめ
TypeScriptの高度な型システムを活用することで、以下のメリットが得られます:
型安全性の向上
- ✅ コンパイル時エラー検出 - 実行前にバグを発見
- ✅ リファクタリングの安全性 - 型システムがコード変更を追跡
- ✅ APIの明確化 - 関数のインターフェースが自己文書化
開発体験の改善
- ✅ IntelliSenseの強化 - エディタの自動補完が正確に
- ✅ ドキュメント不要 - 型定義がドキュメントの役割を果たす
- ✅ チーム開発の効率化 - 型が仕様を明確にする
実践的な使い方
- ✅ ジェネリック - 再利用可能で型安全なコード
- ✅ 条件付き型 - 型レベルでのロジック実装
- ✅ 型ガード - 実行時の型安全性
- ✅ ユーティリティ型 - 定型パターンの簡潔な記述
🔗 参考リンク
型システムを深く理解することで、より堅牢で保守性の高いコードを書けるようになります!
#TypeScript#Type System#Advanced#Generics#Type Safety
著者について
kitahara-devによって執筆されました。