mcpField
T
typescript-advanced-types

typescript-advanced-types

github.com/rolling-scopes開発10.4k36 回インストール6 日前に更新
リポジトリ

typescript-advanced-types について

TypeScript の高度な型システムを扱うスキルです。型安全なアプリケーションを作る開発者向けに、推論や再利用可能な型パターン、テストを解説します。

主な機能

  1. 01ジェネリック型と条件型
  2. 02マップ型とテンプレートリテラル型
  3. 03ユーティリティ型と再帰型
  4. 04型推論と型ガード
  5. 05型テストとベストプラクティス

活用シーン

  1. 01型安全な API クライアントの構築
  2. 02再利用可能なジェネリックコンポーネントの作成
  3. 03フォーム検証型の設計
  4. 04JavaScript から TypeScript への移行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rolling-scopes/rsschool-app --skill typescript-advanced-types

インストール後、Claude Code で/typescript-advanced-typesを入力して読み込まれたか確認します。

詳細

TypeScript の高度な型

ジェネリクス、条件型、マップ型、テンプレートリテラル型、ユーティリティ型など、堅牢で型安全なアプリケーションを構築するために、TypeScript の高度な型システムを習得するための包括的なガイドです。

このスキルを使用する場面

  • 型安全なライブラリやフレームワークを構築するとき
  • 再利用可能なジェネリックコンポーネントを作成するとき
  • 複雑な型推論ロジックを実装するとき
  • 型安全な API クライアントを設計するとき
  • フォーム検証システムを構築するとき
  • 強く型付けされた設定オブジェクトを作成するとき
  • 型安全な状態管理を実装するとき
  • JavaScript のコードベースを TypeScript に移行するとき

コアコンセプト

1. ジェネリクス

目的: 型安全性を維持しながら、再利用可能で柔軟な型を持つコンポーネントを作成します。

基本的なジェネリック関数:

function identity<T>(value: T): T {
  return value;
}

const num = identity<number>(42); // Type: number
const str = identity<string>('hello'); // Type: string
const auto = identity(true); // Type inferred: boolean

ジェネリック制約:

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): T {
  console.log(item.length);
  return item;
}

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 has no length

複数の型パラメーター:

function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const merged = merge({ name: 'John' }, { age: 30 });
// Type: { name: string } & { age: number }
2. 条件型

目的: 条件に応じて変化する型を作成し、高度な型ロジックを実現します。

基本的な条件型:

type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

戻り値の型の抽出:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
  return { id: 1, name: 'John' };
}

type User = ReturnType<typeof getUser>;
// Type: { id: number; name: string; }

分配条件型:

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

type StrOrNumArray = ToArray<string | number>;
// Type: string[] | number[]

ネストされた条件:

type TypeName<T> = T extends string
  ? 'string'
  : T extends number
    ? 'number'
    : T extends boolean
      ? 'boolean'
      : T extends undefined
        ? 'undefined'
        : T extends Function
          ? 'function'
          : 'object';

type T1 = TypeName<string>; // "string"
type T2 = TypeName<() => void>; // "function"
3. マップ型

目的: プロパティを反復処理して、既存の型を変換します。

基本的なマップ型:

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

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

type ReadonlyUser = Readonly<User>;
// Type: { readonly id: number; readonly name: string; }

オプショナルプロパティ:

type Partial<T> = {
  [P in keyof T]?: T[P];
};

type PartialUser = Partial<User>;
// Type: { id?: number; name?: string; }

キーの再マッピング:

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

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// Type: { getName: () => string; getAge: () => number; }

プロパティのフィルタリング:

type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Mixed {
  id: number;
  name: string;
  age: number;
  active: boolean;
}

type OnlyNumbers = PickByType<Mixed, number>;
// Type: { id: number; age: number; }
4. テンプレートリテラル型

目的: パターンマッチングと変換を行う文字列ベースの型を作成します。

基本的なテンプレートリテラル:

type EventName = 'click' | 'focus' | 'blur';
type EventHandler = `on${Capitalize<EventName>}`;
// Type: "onClick" | "onFocus" | "onBlur"

文字列操作:

type UppercaseGreeting = Uppercase<'hello'>; // "HELLO"
type LowercaseGreeting = Lowercase<'HELLO'>; // "hello"
type CapitalizedName = Capitalize<'john'>; // "John"
type UncapitalizedName = Uncapitalize<'John'>; // "john"

パスの構築:

type Path<T> = T extends object
  ? {
      [K in keyof T]: K extends string ? `${K}` | `${K}.${Path<T[K]>}` : never;
    }[keyof T]
  : never;

interface Config {
  server: {
    host: string;
    port: number;
  };
  database: {
    url: string;
  };
}

type ConfigPath = Path<Config>;
// Type: "server" | "database" | "server.host" | "server.port" | "database.url"
5. ユーティリティ型

組み込みユーティリティ型:

// Partial<T> - Make all properties optional
type PartialUser = Partial<User>;

// Required<T> - Make all properties required
type RequiredUser = Required<PartialUser>;

// Readonly<T> - Make all properties readonly
type ReadonlyUser = Readonly<User>;

// Pick<T, K> - Select specific properties
type UserName = Pick<User, 'name' | 'email'>;

// Omit<T, K> - Remove specific properties
type UserWithoutPassword = Omit<User, 'password'>;

// Exclude<T, U> - Exclude types from union
type T1 = Exclude<'a' | 'b' | 'c', 'a'>; // "b" | "c"

// Extract<T, U> - Extract types from union
type T2 = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // "a" | "b"

// NonNullable<T> - Exclude null and undefined
type T3 = NonNullable<string | null | undefined>; // string

// Record<K, T> - Create object type with keys K and values T
type PageInfo = Record<'home' | 'about', { title: string }>;

高度なパターン

パターン 1:型安全なイベントエミッター
type EventMap = {
  'user:created': { id: string; name: string };
  'user:updated': { id: string };
  'user:deleted': { id: string };
};

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, callback: (data: T[K]) => void): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(callback);
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    const callbacks = this.listeners[event];
    if (callbacks) {
      callbacks.forEach(callback => callback(data));
    }
  }
}

const emitter = new TypedEventEmitter<EventMap>();

emitter.on('user:created', data => {
  console.log(data.id, data.name); // Type-safe!
});

emitter.emit('user:created', { id: '1', name: 'John' });
// emitter.emit("user:created", { id: "1" });  // Error: missing 'name'
パターン 2:型安全な API クライアント
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

type EndpointConfig = {
  '/users': {
    GET: { response: User[] };
    POST: { body: { name: string; email: string }; response: User };
  };
  '/users/:id': {
    GET: { params: { id: string }; response: User };
    PUT: { params: { id: string }; body: Partial<User>; response: User };
    DELETE: { params: { id: string }; response: void };
  };
};

type ExtractParams<T> = T extends { params: infer P } ? P : never;
type ExtractBody<T> = T extends { body: infer B } ? B : never;
type ExtractResponse<T> = T extends { response: infer R } ? R : never;

class APIClient<Config extends Record<string, Record<HTTPMethod, any>>> {
  async request<Path extends keyof Config, Method extends keyof Config[Path]>(
    path: Path,
    method: Method,
    ...[options]: ExtractParams<Config[Path][Method]> extends never
      ? ExtractBody<Config[Path][Method]> extends never
        ? []
        : [{ body: ExtractBody<Config[Path][Method]> }]
      : [
          {
            params: ExtractParams<Config[Path][Method]>;
            body?: ExtractBody<Config[Path][Method]>;
          },
        ]
  ): Promise<ExtractResponse<Config[Path][Method]>> {
    // Implementation here
    return {} as any;
  }
}

const api = new APIClient<EndpointConfig>();

// Type-safe API calls
const users = await api.request('/users', 'GET');
// Type: User[]

const newUser = await api.request('/users', 'POST', {
  body: { name: 'John', email: '[email protected]' },
});
// Type: User

const user = await api.request('/users/:id', 'GET', {
  params: { id: '123' },
});
// Type: User
パターン 3:型安全なビルダーパターン
type BuilderState<T> = {
  [K in keyof T]: T[K] | undefined;
};

type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

type IsComplete<T, S> = RequiredKeys<T> extends keyof S ? (S[RequiredKeys<T>] extends undefined ? false : true) : false;

class Builder<T, S extends BuilderState<T> = {}> {
  private state: S = {} as S;

  set<K extends keyof T>(key: K, value: T[K]): Builder<T, S & Record<K, T[K]>> {
    this.state[key] = value;
    return this as any;
  }

  build(this: IsComplete<T, S> extends true ? this : never): T {
    return this.state as T;
  }
}

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

const builder = new Builder<User>();

const user = builder.set('id', '1').set('name', 'John').set('email', '[email protected]').build(); // OK: all required fields set

// const incomplete = builder
//   .set("id", "1")
//   .build();  // Error: missing required fields
パターン 4:深い Readonly/Partial
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? (T[P] extends Function ? T[P] : DeepReadonly<T[P]>) : T[P];
};

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepPartial<U>>
      : DeepPartial<T[P]>
    : T[P];
};

interface Config {
  server: {
    host: string;
    port: number;
    ssl: {
      enabled: boolean;
      cert: string;
    };
  };
  database: {
    url: string;
    pool: {
      min: number;
      max: number;
    };
  };
}

type ReadonlyConfig = DeepReadonly<Config>;
// All nested properties are readonly

type PartialConfig = DeepPartial<Config>;
// All nested properties are optional
パターン 5:型安全なフォーム検証
type ValidationRule<T> = {
  validate: (value: T) => boolean;
  message: string;
};

type FieldValidation<T> = {
  [K in keyof T]?: ValidationRule<T[K]>[];
};

type ValidationErrors<T> = {
  [K in keyof T]?: string[];
};

class FormValidator<T extends Record<string, any>> {
  constructor(private rules: FieldValidation<T>) {}

  validate(data: T): ValidationErrors<T> | null {
    const errors: ValidationErrors<T> = {};
    let hasErrors = false;

    for (const key in this.rules) {
      const fieldRules = this.rules[key];
      const value = data[key];

      if (fieldRules) {
        const fieldErrors: string[] = [];

        for (const rule of fieldRules) {
          if (!rule.validate(value)) {
            fieldErrors.push(rule.message);
          }
        }

        if (fieldErrors.length > 0) {
          errors[key] = fieldErrors;
          hasErrors = true;
        }
      }
    }

    return hasErrors ? errors : null;
  }
}

interface LoginForm {
  email: string;
  password: string;
}

const validator = new FormValidator<LoginForm>({
  email: [
    {
      validate: v => v.includes('@'),
      message: 'Email must contain @',
    },
    {
      validate: v => v.length > 0,
      message: 'Email is required',
    },
  ],
  password: [
    {
      validate: v => v.length >= 8,
      message: 'Password must be at least 8 characters',
    },
  ],
});

const errors = validator.validate({
  email: 'invalid',
  password: 'short',
});
// Type: { email?: string[]; password?: string[]; } | null
パターン 6:判別可能なユニオン
type Success<T> = {
  status: 'success';
  data: T;
};

type Error = {
  status: 'error';
  error: string;
};

type Loading = {
  status: 'loading';
};

type AsyncState<T> = Success<T> | Error | Loading;

function handleState<T>(state: AsyncState<T>): void {
  switch (state.status) {
    case 'success':
      console.log(state.data); // Type: T
      break;
    case 'error':
      console.log(state.error); // Type: string
      break;
    case 'loading':
      console.log('Loading...');
      break;
  }
}

// Type-safe state machine
type State =
  | { type: 'idle' }
  | { type: 'fetching'; requestId: string }
  | { type: 'success'; data: any }
  | { type: 'error'; error: Error };

type Event =
  | { type: 'FETCH'; requestId: string }
  | { type: 'SUCCESS'; data: any }
  | { type: 'ERROR'; error: Error }
  | { type: 'RESET' };

function reducer(state: State, event: Event): State {
  switch (state.type) {
    case 'idle':
      return event.type === 'FETCH' ? { type: 'fetching', requestId: event.requestId } : state;
    case 'fetching':
      if (event.type === 'SUCCESS') {
        return { type: 'success', data: event.data };
      }
      if (event.type === 'ERROR') {
        return { type: 'error', error: event.error };
      }
      return state;
    case 'success':
    case 'error':
      return event.type === 'RESET' ? { type: 'idle' } : state;
  }
}

型推論のテクニック

1. Infer キーワード
// Extract array element type
type ElementType<T> = T extends (infer U)[] ? U : never;

type NumArray = number[];
type Num = ElementType<NumArray>; // number

// Extract promise type
type PromiseType<T> = T extends Promise<infer U> ? U : never;

type AsyncNum = PromiseType<Promise<number>>; // number

// Extract function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

function foo(a: string, b: number) {}
type FooParams = Parameters<typeof foo>; // [string, number]
2. 型ガード
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function isArrayOf<T>(value: unknown, guard: (item: unknown) => item is T): value is T[] {
  return Array.isArray(value) && value.every(guard);
}

const data: unknown = ['a', 'b', 'c'];

if (isArrayOf(data, isString)) {
  data.forEach(s => s.toUpperCase()); // Type: string[]
}
3. アサーション関数
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Not a string');
  }
}

function processValue(value: unknown) {
  assertIsString(value);
  // value is now typed as string
  console.log(value.toUpperCase());
}

ベストプラクティス

  1. any より unknown を使用する:型チェックを強制します
  2. オブジェクトの形状には interface を優先する:より分かりやすいエラーメッセージが得られます
  3. ユニオンや複雑な型には type を使用する:より柔軟に扱えます
  4. 型推論を活用する:可能な場合は TypeScript に推論させます
  5. ヘルパー型を作成する:再利用可能な型ユーティリティを構築します
  6. const アサーションを使用する:リテラル型を保持します
  7. 型アサーションを避ける:代わりに型ガードを使用します
  8. 複雑な型をドキュメント化する:JSDoc コメントを追加します
  9. strict モードを使用する:すべての strict コンパイラーオプションを有効にします
  10. 型をテストする:型テストを使用して型の動作を検証します

型テスト

// Type assertion tests
type AssertEqual<T, U> = [T] extends [U] ? ([U] extends [T] ? true : false) : false;

type Test1 = AssertEqual<string, string>; // true
type Test2 = AssertEqual<string, number>; // false
type Test3 = AssertEqual<string | number, string>; // false

// Expect error helper
type ExpectError<T extends never> = T;

// Example usage
type ShouldError = ExpectError<AssertEqual<string, number>>;

よくある落とし穴

  1. any の多用:TypeScript を使う目的が損なわれます
  2. strict null チェックの無視:実行時エラーにつながる可能性があります
  3. 複雑すぎる型:コンパイルが遅くなる可能性があります
  4. 判別可能なユニオンを使用しない:型の絞り込みの機会を逃します
  5. readonly 修飾子を忘れる:意図しない変更が可能になります
  6. 循環する型参照:コンパイラーエラーが発生する可能性があります
  7. エッジケースを処理しない:空の配列や null 値などを見落とします

パフォーマンスに関する考慮事項

  • 深くネストされた条件型は避けてください
  • 可能な場合は単純な型を使用してください
  • 複雑な型計算をキャッシュしてください
  • 再帰型の再帰の深さを制限してください
  • 本番環境では型チェックを省略できるビルドツールを使用してください

リソース

コメント