TypeScript Utility Types【関数・Promise編】

に公開

はじめに

この記事では、TypeScript の関数や Promise に特化した Utility Types を実例を交えて詳しく解説していきます。

関数に関する Utility Types

ReturnType<T> - 関数の戻り値の型を取得

ReturnType<T>は、関数の戻り値の型を抽出します。

function getUser() {
  return { name: "John", age: 30, email: "john@example.com" };
}

type User = ReturnType<typeof getUser>;
// User は { name: string; age: number; email: string; } になる

// 使用例:APIレスポンス型の統一
async function fetchUserData() {
  const response = await fetch("/api/user");
  return response.json();
}

type UserData = ReturnType<typeof fetchUserData>;
// UserData は Promise<any> になる

// より実践的な例
function createApiClient() {
  return {
    getUsers: () => Promise.resolve([{ id: 1, name: "Alice" }]),
    getUser: (id: number) => Promise.resolve({ id, name: "Alice" }),
    deleteUser: (id: number) => Promise.resolve({ success: true }),
  };
}

type ApiClient = ReturnType<typeof createApiClient>;
// ApiClient は作成されたオブジェクトの型になる

Parameters<T> - 関数の引数の型をタプルで取得

Parameters<T>は、関数の引数の型をタプルとして抽出します。

function createUser(name: string, age: number, email: string) {
  return { name, age, email };
}

type CreateUserParams = Parameters<typeof createUser>;
// CreateUserParams は [string, number, string] になる

// 使用例:関数のラッパーを作る
function logAndCreateUser(...args: Parameters<typeof createUser>) {
  console.log("Creating user with params:", args);
  return createUser(...args);
}

// より複雑な例:汎用的なAPI呼び出し関数
async function apiCall(
  endpoint: string,
  method: "GET" | "POST" | "PUT" | "DELETE",
  data?: any
) {
  // API呼び出しロジック
  return { success: true, data: null };
}

type ApiCallParams = Parameters<typeof apiCall>;
// [string, 'GET' | 'POST' | 'PUT' | 'DELETE', any?]

function createApiCallWrapper(defaultEndpoint: string) {
  return function (method: ApiCallParams[1], data?: ApiCallParams[2]) {
    return apiCall(defaultEndpoint, method, data);
  };
}

ConstructorParameters<T> - コンストラクタの引数の型を取得

ConstructorParameters<T>は、クラスのコンストラクタの引数の型をタプルとして抽出します。

class User {
  constructor(public name: string, public age: number, public email: string) {}
}

type UserConstructorParams = ConstructorParameters<typeof User>;
// UserConstructorParams は [string, number, string] になる

// 使用例:ファクトリー関数
function createUser(...args: ConstructorParameters<typeof User>) {
  return new User(...args);
}

// より実践的な例:依存注入
class DatabaseService {
  constructor(private connectionString: string, private timeout: number) {}
}

class UserService {
  constructor(
    private db: DatabaseService,
    private cacheTimeout: number = 300
  ) {}
}

type UserServiceDeps = ConstructorParameters<typeof UserService>;
// [DatabaseService, number?]

function createUserService(...deps: UserServiceDeps) {
  return new UserService(...deps);
}

InstanceType<T> - コンストラクタの戻り値の型を取得

InstanceType<T>は、コンストラクタ関数のインスタンスの型を抽出します。

class User {
  constructor(public name: string, public age: number) {}

  greet() {
    return `Hello, I'm ${this.name}`;
  }
}

type UserInstance = InstanceType<typeof User>;
// UserInstance は User になる

// 使用例:汎用的なインスタンス生成関数
function createInstance<T extends new (...args: any[]) => any>(
  ctor: T,
  ...args: ConstructorParameters<T>
): InstanceType<T> {
  return new ctor(...args);
}

const user = createInstance(User, "Alice", 25);
// user の型は User

// より実践的な例:プラグインシステム
interface Plugin {
  name: string;
  execute(): void;
}

class LoggerPlugin implements Plugin {
  constructor(public name: string) {}
  execute() {
    console.log(`[${this.name}] Logging...`);
  }
}

class DatabasePlugin implements Plugin {
  constructor(public name: string, private connectionString: string) {}
  execute() {
    console.log(`[${this.name}] Connecting to database...`);
  }
}

type PluginConstructors = typeof LoggerPlugin | typeof DatabasePlugin;
type PluginInstance = InstanceType<PluginConstructors>;
// PluginInstance は LoggerPlugin | DatabasePlugin

NoInfer<T> - 型推論を防ぐ

NoInfer<T>は、TypeScript 5.4 で追加された型で、型推論を防ぐために使用します。

// 問題のある例:TypeScriptは最後の引数から型を推論してしまう
function createConfig<T extends string>(
  options: T[],
  defaultOption: T // ここでTが推論されてしまう
) {
  return { options, defaultOption };
}

// "blue"が存在しないため、Tが"red" | "green" | "blue"に拡張されてしまう
const config1 = createConfig(["red", "green"], "blue"); // エラーにならない

// NoInfer<T>を使用した解決例
function createConfigSafe<T extends string>(
  options: T[],
  defaultOption: NoInfer<T> // 型推論を防ぐ
) {
  return { options, defaultOption };
}

// 今度は正しくエラーになる
const config2 = createConfigSafe(["red", "green"], "red"); // ✅ OK
// const config3 = createConfigSafe(["red", "green"], "blue"); // ❌ エラー

// 実用例:型安全な設定システム
interface ThemeConfig {
  colors: string[];
  defaultColor: string;
  fonts: string[];
  defaultFont: string;
}

function createTheme<TColors extends string, TFonts extends string>(
  colors: TColors[],
  defaultColor: NoInfer<TColors>,
  fonts: TFonts[],
  defaultFont: NoInfer<TFonts>
): ThemeConfig {
  return {
    colors,
    defaultColor,
    fonts,
    defaultFont,
  };
}

const theme = createTheme(
  ["red", "blue", "green"],
  "red", // ✅ colors配列に存在する
  ["Arial", "Helvetica"],
  "Arial" // ✅ fonts配列に存在する
);

// const invalidTheme = createTheme(
//   ["red", "blue", "green"],
//   "purple",  // ❌ エラー: colors配列に存在しない
//   ["Arial", "Helvetica"],
//   "Times"    // ❌ エラー: fonts配列に存在しない
// );

Promise に関する Utility Types

Awaited<T> - Promise の解決値の型を取得

Awaited<T>は、Promise が解決する値の型を抽出します。ネストした Promise にも対応しています。

// 基本的な使用例
type Result = Awaited<Promise<string>>;
// Result は string になる

// ネストしたPromiseの場合
type NestedResult = Awaited<Promise<Promise<number>>>;
// NestedResult は number になる(ネストが解消される)

// 使用例:非同期関数の戻り値型を取得
async function fetchUserProfile() {
  const response = await fetch("/api/user/profile");
  return response.json() as {
    id: number;
    name: string;
    avatar: string;
  };
}

type UserProfile = Awaited<ReturnType<typeof fetchUserProfile>>;
// UserProfile は { id: number; name: string; avatar: string; } になる

// より複雑な例:複数の非同期処理の結果
async function fetchUserData() {
  const [profile, settings, notifications] = await Promise.all([
    fetch("/api/profile").then((r) => r.json()),
    fetch("/api/settings").then((r) => r.json()),
    fetch("/api/notifications").then((r) => r.json()),
  ]);

  return { profile, settings, notifications };
}

type UserData = Awaited<ReturnType<typeof fetchUserData>>;
// UserData は各プロパティの型を持つオブジェクトになる

その他の関数関連 Utility Types

ThisParameterType<T> - 関数の this パラメータの型を取得

function greet(this: { name: string }, message: string) {
  return `${this.name}: ${message}`;
}

type ThisType = ThisParameterType<typeof greet>;
// ThisType は { name: string } になる

OmitThisParameter<T> - 関数から this パラメータを除去

function greet(this: { name: string }, message: string) {
  return `${this.name}: ${message}`;
}

type GreetWithoutThis = OmitThisParameter<typeof greet>;
// GreetWithoutThis は (message: string) => string になる

実践的な使用例

型安全なイベントハンドラー

interface EventMap {
  click: { x: number; y: number };
  keydown: { key: string; ctrlKey: boolean };
  resize: { width: number; height: number };
}

type EventListener<T extends keyof EventMap> = (event: EventMap[T]) => void;

class EventEmitter {
  on<T extends keyof EventMap>(event: T, listener: EventListener<T>): void {
    // イベントリスナーの登録
  }

  emit<T extends keyof EventMap>(
    event: T,
    data: Parameters<EventListener<T>>[0]
  ): void {
    // イベントの発行
  }
}

// 使用例
const emitter = new EventEmitter();
emitter.on("click", ({ x, y }) => {
  console.log(`Clicked at ${x}, ${y}`);
});

emitter.emit("click", { x: 100, y: 200 }); // ✅ 型安全
// emitter.emit('click', { x: 100 }); // ❌ エラー: y プロパティがない

型安全な API クライアント

interface ApiEndpoints {
  "/users": {
    GET: { users: Array<{ id: number; name: string }> };
    POST: { user: { id: number; name: string } };
  };
  "/users/:id": {
    GET: { user: { id: number; name: string; email: string } };
    PUT: { user: { id: number; name: string; email: string } };
    DELETE: { success: boolean };
  };
}

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

type ApiResponse<
  TEndpoint extends keyof ApiEndpoints,
  TMethod extends HttpMethod
> = TMethod extends keyof ApiEndpoints[TEndpoint]
  ? ApiEndpoints[TEndpoint][TMethod]
  : never;

async function apiCall<
  TEndpoint extends keyof ApiEndpoints,
  TMethod extends HttpMethod
>(
  endpoint: TEndpoint,
  method: TMethod,
  data?: any
): Promise<ApiResponse<TEndpoint, TMethod>> {
  // API呼び出しの実装
  return {} as any;
}

// 使用例
const userList = await apiCall("/users", "GET");
// userList の型は { users: Array<{ id: number; name: string }> }

const userDetail = await apiCall("/users/:id", "GET");
// userDetail の型は { user: { id: number; name: string; email: string } }

まとめ

この関数・Promise 編では、関数や Promise に特化した 8 つの Utility Types を紹介しました:

  • ReturnType<T> - 関数の戻り値の型を取得
  • Parameters<T> - 関数の引数の型をタプルで取得
  • ConstructorParameters<T> - コンストラクタの引数の型を取得
  • InstanceType<T> - コンストラクタの戻り値の型を取得
  • NoInfer<T> - 型推論を防ぐ(TypeScript 5.4 以降)
  • Awaited<T> - Promise の解決値の型を取得
  • ThisParameterType<T> - 関数の this パラメータの型を取得
  • OmitThisParameter<T> - 関数から this パラメータを除去

これらを活用することで、関数や Promise を扱う際の型安全性を大幅に向上させることができます。

参考文献

https://www.typescriptlang.org/docs/handbook/utility-types.html
https://www.typescriptlang.org/docs/handbook/2/functions.html

Discussion