📚

TypeScript型ガイド - 実践で使える型定義パターン集

に公開

はじめに

「あの型の書き方、なんだったっけ?」「このケースはどの型を使えばいいの?」

TypeScript を使っていると、型定義で悩むことがよくありますよね。この記事は、そんな時にすぐに参照できる実践的な型定義ガイドとして作成しました。

基本型(よく使う型)

プリミティブ型

// 文字列型
let name: string = "田中太郎";
let template: string = `Hello, ${name}`;

// 数値型
let age: number = 25;
let pi: number = 3.14;
let hex: number = 0xff; // 16進数
let binary: number = 0b1010; // 2進数

// 真偽値型
let isActive: boolean = true;
let isCompleted: boolean = false;

// BigInt型(大きな整数)
let bigNumber: bigint = 9007199254740991n;

// Symbol型(ユニークな識別子)
let uniqueId: symbol = Symbol("id");

// null と undefined
let empty: null = null;
let notSet: undefined = undefined;

リテラル型(特定の値のみ許可)

// 文字列リテラル型
type Theme = "light" | "dark" | "auto";
let currentTheme: Theme = "dark"; // ✅ OK
// let invalidTheme: Theme = "blue"; // ❌ Error

// 数値リテラル型
type StatusCode = 200 | 404 | 500;
let responseStatus: StatusCode = 200;

// 真偽値リテラル型
type StrictTrue = true;
let flag: StrictTrue = true;
// let flag2: StrictTrue = false; // ❌ Error

// 混合リテラル型
type Mixed = "success" | 200 | true;

テンプレートリテラル型(文字列パターン)

// 基本的なテンプレートリテラル型
type EventName = `on${Capitalize<string>}`;
type ClickEvent = `onClick`; // "onClick"
type HoverEvent = `onHover`; // "onHover"

// 複数の値を組み合わせ
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiEndpoint = `/api/${string}`;
type ApiUrl = `https://api.example.com${ApiEndpoint}`;

// 実用的な例:CSS プロパティ
type CSSUnit = "px" | "em" | "rem" | "%";
type CSSValue = `${number}${CSSUnit}`;
let margin: CSSValue = "16px"; // ✅ OK
// let invalidMargin: CSSValue = "16"; // ❌ Error

配列とオブジェクトの型

配列型の書き方

// 基本的な配列型
let numbers: number[] = [1, 2, 3, 4, 5];
let strings: Array<string> = ["apple", "banana", "orange"];

// 複数の型を許可する配列
let mixed: (string | number)[] = ["text", 42, "hello", 100];

// 読み取り専用配列
let readonlyNumbers: readonly number[] = [1, 2, 3];
let readonlyStrings: ReadonlyArray<string> = ["a", "b", "c"];
// readonlyNumbers.push(4); // ❌ Error: 変更不可

// 多次元配列
let matrix: number[][] = [
  [1, 2],
  [3, 4],
  [5, 6],
];
let cube: number[][][] = [[[1, 2]], [[3, 4]]];

タプル型(要素数と型が固定)

// 基本的なタプル
let coordinate: [number, number] = [10, 20];
let userInfo: [string, number, boolean] = ["Alice", 25, true];

// 名前付きタプル(TypeScript 4.0+)
let namedTuple: [name: string, age: number] = ["Bob", 30];

// 可変長タプル
let variableTuple: [string, ...number[]] = ["prefix", 1, 2, 3];

// オプショナル要素
let optionalTuple: [string, number?] = ["test"]; // 2番目の要素は省略可能

// 読み取り専用タプル
let readonlyTuple: readonly [string, number] = ["readonly", 42];

オブジェクト型の定義

// インライン型定義
let user: { name: string; age: number; email?: string } = {
  name: "田中太郎",
  age: 30,
  email: "tanaka@example.com",
};

// インターフェースを使った定義
interface User {
  readonly id: number; // 読み取り専用
  name: string;
  age: number;
  email?: string; // オプショナル
  tags: string[];
  profile: {
    bio: string;
    avatar?: string;
  };
}

// type エイリアスを使った定義
type Product = {
  id: string;
  name: string;
  price: number;
  category: "electronics" | "clothing" | "books";
  specifications: Record<string, string>; // 任意のキー・バリュー
};

// インデックスシグネチャ(動的なプロパティ)
interface Dictionary {
  [key: string]: string;
}

interface NumberDictionary {
  [index: number]: string;
  length: number; // 固定プロパティも併用可能
}

関数の型定義

基本的な関数型

// 関数宣言
function add(a: number, b: number): number {
  return a + b;
}

// 関数式
const multiply = (a: number, b: number): number => a * b;

// 関数型を型として定義
type MathOperation = (a: number, b: number) => number;
const divide: MathOperation = (a, b) => a / b;

// より複雑な関数型
type AsyncDataFetcher<T> = (url: string) => Promise<T>;
const fetchUser: AsyncDataFetcher<User> = async (url) => {
  const response = await fetch(url);
  return response.json();
};

オプション引数とデフォルト引数

// オプション引数
function greet(name: string, greeting?: string): string {
  return `${greeting || "Hello"}, ${name}!`;
}

// デフォルト引数
function createUser(name: string, age: number = 0): User {
  return { name, age };
}

// Rest パラメータ
function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}

// 分割代入のパラメータ
function processUser({ name, age }: { name: string; age: number }): string {
  return `${name} is ${age} years old`;
}

関数のオーバーロード

// 関数オーバーロードの宣言
function format(value: string): string;
function format(value: number): string;
function format(value: Date): string;

// 実装
function format(value: string | number | Date): string {
  if (typeof value === "string") {
    return value.toUpperCase();
  } else if (typeof value === "number") {
    return value.toFixed(2);
  } else {
    return value.toISOString();
  }
}

// 使用例
const formatted1 = format("hello"); // string
const formatted2 = format(42); // string
const formatted3 = format(new Date()); // string

ユニオンとインターセクション型

ユニオン型(OR 演算)

// 基本的なユニオン型
type StringOrNumber = string | number;
let value: StringOrNumber = "text"; // または 42

// 複数の型のユニオン
type Status = "loading" | "success" | "error";
type Response = string | number | boolean | null;

// オブジェクトのユニオン型(判別可能ユニオン)
type ApiResponse =
  | { status: "success"; data: any }
  | { status: "error"; message: string }
  | { status: "loading" };

// 型ガード関数
function isSuccessResponse(
  response: ApiResponse
): response is { status: "success"; data: any } {
  return response.status === "success";
}

// 使用例
function handleResponse(response: ApiResponse) {
  if (response.status === "success") {
    console.log(response.data); // TypeScriptが型を推論
  } else if (response.status === "error") {
    console.log(response.message);
  }
}

インターセクション型(AND 演算)

// 基本的なインターセクション型
type Person = {
  name: string;
  age: number;
};

type Employee = {
  employeeId: string;
  department: string;
};

type PersonEmployee = Person & Employee;
// = { name: string; age: number; employeeId: string; department: string; }

// 複数の型を組み合わせ
type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};

type FullUserData = Person & Employee & Timestamped;

// ミックスイン的な使用
type WithId = { id: string };
type WithTimestamp = { timestamp: Date };

type BlogPost = {
  title: string;
  content: string;
} & WithId &
  WithTimestamp;

実用的なユーティリティ型

基本的なユーティリティ型

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

// Partial<T> - すべてのプロパティをオプショナルに
type PartialUser = Partial<User>;
// = { id?: string; name?: string; email?: string; age?: number; profile?: { bio: string; avatar?: string; }; }

function updateUser(id: string, updates: Partial<User>) {
  // ユーザーの一部だけ更新
}

// Required<T> - すべてのプロパティを必須に
type RequiredUser = Required<User>;
// profile.avatar も必須になる

// Pick<T, K> - 指定したプロパティのみ選択
type UserSummary = Pick<User, "id" | "name" | "email">;
// = { id: string; name: string; email: string; }

// Omit<T, K> - 指定したプロパティを除外
type PublicUser = Omit<User, "id" | "email">;
// = { name: string; age: number; profile: { bio: string; avatar?: string; }; }

// Record<K, T> - キーと値の型を指定した辞書型
type UserRoles = Record<string, "admin" | "user" | "guest">;
// = { [x: string]: "admin" | "user" | "guest"; }

const roles: UserRoles = {
  user1: "admin",
  user2: "user",
  user3: "guest",
};

関数関連のユーティリティ型

// 関数の例
function fetchUserData(id: string, includeProfile: boolean): Promise<User> {
  // 実装...
  return Promise.resolve({} as User);
}

// Parameters<T> - 関数の引数の型をタプルとして取得
type FetchParams = Parameters<typeof fetchUserData>;
// = [id: string, includeProfile: boolean]

// ReturnType<T> - 関数の戻り値の型を取得
type FetchReturn = ReturnType<typeof fetchUserData>;
// = Promise<User>

// 実用例:APIクライアントの型定義
type ApiClient = {
  getUser: (id: string) => Promise<User>;
  updateUser: (id: string, data: Partial<User>) => Promise<User>;
  deleteUser: (id: string) => Promise<void>;
};

type GetUserParams = Parameters<ApiClient["getUser"]>;
type UpdateUserParams = Parameters<ApiClient["updateUser"]>;

条件型(Conditional Types)

// 基本的な条件型
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false

// 実用的な条件型
type NonNullable<T> = T extends null | undefined ? never : T;
type SafeString = NonNullable<string | null>; // string

// 配列の要素型を取得
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type StringArrayElement = ArrayElement<string[]>; // string
type NumberArrayElement = ArrayElement<number[]>; // number

// Promiseの中身を取得
type Awaited<T> = T extends Promise<infer U> ? U : T;
type UserData = Awaited<Promise<User>>; // User
type StringData = Awaited<string>; // string

Mapped Types(型変換)

// 基本的なMapped Types
type Optional<T> = {
  [K in keyof T]?: T[K];
};

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

// 実用的なMapped Types
type Stringify<T> = {
  [K in keyof T]: string;
};

type StringifiedUser = Stringify<User>;
// = { id: string; name: string; email: string; age: string; profile: string; }

// キー名を変換するMapped Types
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<{ name: string; age: number }>;
// = { getName: () => string; getAge: () => number; }

// 条件付きMapped Types
type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

type UserStrings = PickByType<User, string>;
// = { id: string; name: string; email: string; }

特殊な型とキーワード

any, unknown, never, void

// any - 型チェックを無効にする(極力避ける)
let anything: any = 42;
anything = "string";
anything = { foo: "bar" };
anything.foo.bar.baz; // エラーにならない(危険!)

// unknown - より安全な any
let something: unknown = 42;
// something.toFixed(); // ❌ Error: 型チェックが必要

if (typeof something === "number") {
  something.toFixed(); // ✅ OK: 型ガード後
}

// never - 絶対に到達しない型
function throwError(): never {
  throw new Error("This function never returns");
}

function exhaustiveCheck(value: "a" | "b"): string {
  switch (value) {
    case "a":
      return "Option A";
    case "b":
      return "Option B";
    default:
      // value は never型になる
      const _exhaustiveCheck: never = value;
      throw new Error(`Unhandled case: ${value}`);
  }
}

// void - 戻り値なし
function logMessage(message: string): void {
  console.log(message);
  // return undefined; // 暗黙的に undefined を返す
}

keyof 演算子

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

// keyof で型のキーを取得
type UserKeys = keyof User; // "id" | "name" | "email" | "age"

// 実用例:型安全なプロパティアクセス
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = {
  id: "1",
  name: "Alice",
  email: "alice@example.com",
  age: 30,
};
const userName = getProperty(user, "name"); // string型として推論
const userAge = getProperty(user, "age"); // number型として推論
// const invalid = getProperty(user, "invalid"); // ❌ Error

// ネストしたオブジェクトのキー
type NestedKeys<T> = {
  [K in keyof T]: T[K] extends object ? `${string & K}.${NestedKeys<T[K]>}` : K;
}[keyof T];

typeof 演算子

// 値から型を推論
const userConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3,
  headers: {
    "Content-Type": "application/json",
  },
} as const; // as const で readonly に

type UserConfig = typeof userConfig;
// = {
//   readonly apiUrl: "https://api.example.com";
//   readonly timeout: 5000;
//   readonly retries: 3;
//   readonly headers: {
//     readonly "Content-Type": "application/json";
//   };
// }

// 関数の型を取得
function processData(data: string[], options: { sort: boolean }) {
  // 実装...
  return data;
}

type ProcessDataFunction = typeof processData;
// = (data: string[], options: { sort: boolean }) => string[]

in 演算子とプロパティ存在チェック

// in演算子を使った型ガード
interface Cat {
  type: "cat";
  meow(): void;
}

interface Dog {
  type: "dog";
  bark(): void;
}

type Pet = Cat | Dog;

function handlePet(pet: Pet) {
  if ("meow" in pet) {
    pet.meow(); // Cat型として認識
  } else {
    pet.bark(); // Dog型として認識
  }
}

// より実用的な例
function isError(value: unknown): value is Error {
  return value instanceof Error;
}

// プロパティ存在チェックのヘルパー関数
function hasProperty<T, K extends string>(
  obj: T,
  prop: K
): obj is T & Record<K, unknown> {
  return typeof obj === "object" && obj !== null && prop in obj;
}

実践的な型定義パターン

API レスポンスの型定義

// 基本的なAPIレスポンス
interface BaseResponse {
  success: boolean;
  timestamp: string;
}

interface SuccessResponse<T> extends BaseResponse {
  success: true;
  data: T;
}

interface ErrorResponse extends BaseResponse {
  success: false;
  error: {
    code: string;
    message: string;
    details?: Record<string, any>;
  };
}

type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;

// 使用例
async function fetchUsers(): Promise<ApiResponse<User[]>> {
  const response = await fetch("/api/users");
  return response.json();
}

// レスポンス処理
function handleUserResponse(response: ApiResponse<User[]>) {
  if (response.success) {
    // response.data は User[] 型
    console.log(`取得したユーザー数: ${response.data.length}`);
  } else {
    // response.error は存在する
    console.error(`エラー: ${response.error.message}`);
  }
}

設定オブジェクトの型定義

// 段階的な設定の型定義
interface DatabaseConfig {
  host: string;
  port: number;
  database: string;
  ssl?: boolean;
}

interface AuthConfig {
  type: "oauth" | "basic" | "jwt";
  credentials: {
    oauth: { clientId: string; clientSecret: string };
    basic: { username: string; password: string };
    jwt: { secret: string; expiresIn: string };
  }[AuthConfig["type"]];
}

interface AppConfig {
  env: "development" | "production" | "test";
  database: DatabaseConfig;
  auth: AuthConfig;
  features: {
    enableLogging: boolean;
    enableMetrics: boolean;
    enableCaching: boolean;
  };
}

// 環境ごとの設定型
type DevelopmentConfig = AppConfig & {
  env: "development";
  features: AppConfig["features"] & {
    enableDebugMode: boolean;
  };
};

状態管理の型定義

// Redux的な状態管理の型
interface AppState {
  user: {
    currentUser: User | null;
    isLoading: boolean;
    error: string | null;
  };
  posts: {
    items: Post[];
    isLoading: boolean;
    error: string | null;
  };
}

// アクションの型定義
type UserAction =
  | { type: "USER_FETCH_START" }
  | { type: "USER_FETCH_SUCCESS"; payload: User }
  | { type: "USER_FETCH_ERROR"; payload: string }
  | { type: "USER_LOGOUT" };

type PostAction =
  | { type: "POSTS_FETCH_START" }
  | { type: "POSTS_FETCH_SUCCESS"; payload: Post[] }
  | { type: "POSTS_ADD"; payload: Post }
  | { type: "POSTS_DELETE"; payload: string };

type AppAction = UserAction | PostAction;

// Reducer の型定義
type Reducer<S, A> = (state: S, action: A) => S;

const userReducer: Reducer<AppState["user"], UserAction> = (state, action) => {
  switch (action.type) {
    case "USER_FETCH_SUCCESS":
      return { ...state, currentUser: action.payload, isLoading: false };
    // その他のケース...
    default:
      return state;
  }
};

高度な型テクニック

型レベルプログラミング

// 文字列操作の型
type Uppercase<S extends string> = Intrinsic; // TypeScript組み込み
type Lowercase<S extends string> = Intrinsic; // TypeScript組み込み
type Capitalize<S extends string> = Intrinsic; // TypeScript組み込み
type Uncapitalize<S extends string> = Intrinsic; // TypeScript組み込み

// 実用例:APIエンドポイントの型生成
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = `/api/${"users" | "posts" | "comments"}`;
type ApiCall = `${HttpMethod} ${Endpoint}`;

// 文字列から型を生成
type ParseRoute<T extends string> =
  T extends `${infer Method} /api/${infer Resource}`
    ? { method: Method; resource: Resource }
    : never;

type UserGetCall = ParseRoute<"GET /api/users">;
// = { method: "GET"; resource: "users" }

再帰的な型定義

// JSON型の完全な定義
type JsonPrimitive = string | number | boolean | null;
type JsonObject = { [key: string]: JsonValue };
type JsonArray = JsonValue[];
type JsonValue = JsonPrimitive | JsonObject | JsonArray;

// 深くネストしたオブジェクトのパス
type DeepPaths<T, K extends keyof T = keyof T> = K extends string
  ? T[K] extends Record<string, any>
    ? `${K}` | `${K}.${DeepPaths<T[K]>}`
    : `${K}`
  : never;

type UserPaths = DeepPaths<{
  profile: {
    personal: {
      name: string;
      age: number;
    };
    social: {
      twitter: string;
    };
  };
  settings: {
    theme: string;
  };
}>;
// = "profile" | "settings" | "profile.personal" | "profile.social" |
//   "profile.personal.name" | "profile.personal.age" | "profile.social.twitter" | "settings.theme"

型安全なビルダーパターン

interface QueryBuilder<T = {}> {
  select<K extends string>(fields: K[]): QueryBuilder<T & { select: K[] }>;
  where<K extends string, V>(
    field: K,
    value: V
  ): QueryBuilder<T & { where: Record<K, V> }>;
  orderBy<K extends string>(
    field: K,
    direction: "asc" | "desc"
  ): QueryBuilder<T & { orderBy: { field: K; direction: "asc" | "desc" } }>;
  build(): T extends { select: any } ? T : never; // selectが必須
}

// 使用例
const query = new QueryBuilder()
  .select(["name", "email"])
  .where("age", 25)
  .orderBy("name", "asc")
  .build(); // 型安全にクエリを構築

よく使う型定義のチートシート

型ガード関数

// プリミティブ型の型ガード
const isString = (value: unknown): value is string => typeof value === "string";
const isNumber = (value: unknown): value is number => typeof value === "number";
const isArray = <T>(value: unknown): value is T[] => Array.isArray(value);

// オブジェクトの型ガード
const hasProperty = <T, K extends string>(
  obj: T,
  prop: K
): obj is T & Record<K, unknown> => {
  return typeof obj === "object" && obj !== null && prop in obj;
};

// より複雑な型ガード
const isUser = (value: unknown): value is User => {
  return (
    typeof value === "object" &&
    value !== null &&
    hasProperty(value, "id") &&
    hasProperty(value, "name") &&
    typeof value.id === "string" &&
    typeof value.name === "string"
  );
};

よく使う型エイリアス

// 便利な型エイリアス
type NonEmptyArray<T> = [T, ...T[]];
type AtLeastOne<T> = [T, ...T[]];
type OneOrMore<T> = T | T[];

// オプショナルの一部だけ必須に
type RequireFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
type UserWithRequiredEmail = RequireFields<User, "email">;

// 特定のフィールドのみオプショナルに
type OptionalFields<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type UserWithOptionalAge = OptionalFields<User, "age">;

// 深い部分をオプショナルに
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

まとめ

この記事では、TypeScript の型システムを実践的な観点から解説しました。

参考リンク

https://www.typescriptlang.org/docs/
https://basarat.gitbook.io/typescript/
https://www.typescriptlang.org/docs/handbook/utility-types.html

Discussion