Web Development

TypeScript高度な型システム完全ガイド - ジェネリック、条件付き型、型ガード徹底解説

kitahara-dev2025/10/3013 min read
TypeScript高度な型システム完全ガイド - ジェネリック、条件付き型、型ガード徹底解説

TypeScript高度な型システム完全ガイド

TypeScriptの型システムは、JavaScriptに静的型付けをもたらす強力なツールです。このガイドでは、初級者から中級者が理解すべき高度な型定義テクニックを、実践的なコード例とともに徹底解説します。

📋 目次

  1. ジェネリック型の基礎と応用
  2. 条件付き型とinferキーワード
  3. マップされた型とキー再マッピング
  4. ユーティリティ型の完全ガイド
  5. テンプレートリテラル型
  6. 型ガードとユーザー定義型ガード
  7. 関数オーバーロード
  8. as const アサーション
  9. 判別可能なユニオン型
  10. 再帰的型
  11. 型レベルプログラミング
  12. 実践的な型パターン

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によって執筆されました。