デザインについての学習メモブログ

TypeScript入門 #3:関数と型、ジェネリクス入門

記事内に広告が含まれています。

TypeScript入門 #3:関数と型、ジェネリクス入門

第1回、第2回では基本的な型を学びました。

TypeScript入門第3回は、基本的な型の理解を超えて、より高度で柔軟なコードを書けるようになる 関数の型定義とジェネリクス をわかりやすく解説します。

本記事は、 関数の基本から、戻り値の型、オプショナル・デフォルト・Rest パラメータ、そして実務でも頻出の ジェネリクス(Generics) の考え方まで丁寧にステップアップ形式で学べる内容です。

TypeScript の力を最大限に引き出し、型安全で再利用性の高い関数設計をマスターしましょう。

Contents

関数の基本的な「型」定義

基本形

TypeScriptでは、JavaScriptと同様に複数の方法で関数を定義できます。

それぞれ書き方・用途・特徴が異なります。

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

// 関数式
const subtract = function(a: number, b: number): number {
  return a - b;
};

// アロー関数
const multiply = (a: number, b: number): number => {
  return a * b;
};

// アロー関数(省略形)
const divide = (a: number, b: number): number => a / b;

どの書き方も

  • 引数 a と b はどちらも number 型
  • : number は「戻り値が数値である」ことを示す型注釈

戻り値の型(neverについても)

TypeScript
// 明示的に戻り値の型を指定
function greet(name: string): string {
  return `Hello, ${name}!`;
}

// 型推論に任せる(戻り値の型は自動的にstringと推論される)
function greet2(name: string) {
  return `Hello, ${name}!`;
}

// 戻り値がない場合はvoid
function logMessage(message: string): void {
  console.log(message);
  // returnなし、またはreturn;のみ
}

// neverは「決して戻らない」関数
function throwError(message: string): never {
  throw new Error(message);
  // この関数は例外をスローするので絶対に戻らない
}

function infiniteLoop(): never {
  while (true) {
    // 無限ループで戻らない
  }
}

never は「このコードパスには絶対に値が存在しない」ことをTypeScriptに伝える型
👉 「never = 到達不能」

以下のような時に使われます。

  • エラー処理
  • 無限ループ
  • union型の網羅チェック

オプショナルパラメータ

TypeScriptの オプショナルパラメータ は、「渡してもいいし、渡さなくてもいい引数」 を表します。

👉 ? を付けると型に undefined が追加される」

TypeScript
// ?を付けるとオプショナル(省略可能)
function greet(name: string, age?: number): string {
  if (age !== undefined) {
    return `Hello, ${name}! You are ${age} years old.`;
  }
  return `Hello, ${name}!`;
}

greet("太郎");           // OK
greet("太郎", 25);       // OK

// 注意:オプショナルパラメータは必須パラメータの後に配置
function invalid(age?: number, name: string) {}  // エラー!

デフォルトパラメータ

TypeScript
// デフォルト値を設定
function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

greet("太郎");                    // "Hello, 太郎!"
greet("太郎", "こんにちは");       // "こんにちは, 太郎!"

// デフォルトパラメータは自動的にオプショナルになる
function createUser(name: string, role: string = "user") {
  return { name, role };
}


デフォルト値を持つ引数も、同じく「省略可能」 として扱われます。

オプショナルとの使い分けの目安

  • 値がなくても処理できる → オプショナル
  • 省略時の挙動を決めたい → デフォルトパラメータ

Restパラメータ

メソッドの宣言で引数の前に”…”とすることで「可変長引数」を受け取ります。
…numbers に渡された引数が 配列 になります。

サンプルコードでは、メソッドの実行時に引数の個数が違っても問題なく受け取る事ができています。

TypeScript
// 任意の数の引数を配列として受け取る
function sum(...numbers: number[]): number {
  return numbers.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3);           // 6
sum(1, 2, 3, 4, 5);     // 15

// 通常のパラメータと組み合わせ
function introduce(greeting: string, ...names: string[]): string {
  return `${greeting}, ${names.join(" and ")}!`;
}

introduce("Hello", "太郎", "花子", "次郎");
// "Hello, 太郎 and 花子 and 次郎!"

オプショナルパラメータとの違い

TypeScript
function example(a?: number, …rest: number[]) {}
  • a → 0 or 1個
  • rest → 0個以上

タプルと組み合わせる(TypeScriptらしい使い方)

TypeScript
function log(…args: [string, number]) {
  const [message, count] = args;
}

log("hello", 3); // OK
log("hello");    // ❌ エラー

👉 引数の数と型を厳密に制御できる

タプルを忘れた方は#2を参照

ジェネリクスと組み合わせる例

TypeScript
function first(…args: T[]): T | undefined {
  return args[0];
}

よくある混同:スプレッド構文との違い

種類使う場所意味
Rest関数定義引数をまとめる
Spread関数呼び出し配列を展開
TypeScript
// Spread
const nums = [1, 2, 3];
sum(…nums);

// Rest
function sum(…nums: number[]) {}

一言でまとめると

Restパラメータ = 「残りの引数を配列で受け取る」

  • 可変長引数を扱える
  • TypeScriptでは型安全に書ける
  • タプルと組み合わせると強力

関数の型

「関数自体」を変数に代入したり、引数として渡したりする場合の型定義です。

関数型の定義

TypeScript
// 方法1:アロー関数形式
//    変数:関数の型
let myAdd: (a: number, b: number) => number;

myAdd = function(x: number, y: number): number {
  return x + y;
};

myAdd(1,3);



// 方法2:型エイリアスを使用
type MathOperation = (a: number, b: number) => number;

let add: MathOperation = (x, y) => x + y;
let subtract: MathOperation = (x, y) => x - y;
let multiply: MathOperation = (x, y) => x * y;



// 関数を引数に取る関数(同じ型エイリアスのaddやsubtractが引数に渡せる)
function calculate(a: number, b: number, operation: MathOperation): number {
  return operation(a, b);
}
calculate(10, 5, add);
  // 15
calculate(10, 5, subtract);
  // 5
calculate(10, 5, multiply);
  // 50

コールバック関数の型定義

TypeScript
// コールバック関数を受け取る
function fetchData(callback: (data: string) => void): void {
  const data = "取得したデータ";
  callback(data);
}

fetchData((data) => {
  console.log(data);
});



// エラーハンドリング付き
type Callback = (error: Error | null, data: string | null) => void;

function fetchDataWithError(callback: Callback): void {
  try {
    const data = "取得したデータ";
    callback(null, data);
  } catch (error) {
    callback(error as Error, null);
  }
}

// 使用例
fetchDataWithError((error, data) => {
  if (error) {
    console.error("エラー:", error);
  } else {
    console.log("データ:", data);
  }
});

関数のオーバーロード

同じ関数名で異なる引数の型を受け入れたい場合に使います。

TypeScript
// オーバーロードシグネチャ
function format(value: string): string;
function format(value: number): string;
function format(value: boolean): string;

// 実装シグネチャ
function format(value: string | number | boolean): string {
  if (typeof value === "string") {
    return `"${value}"`;
  } else if (typeof value === "number") {
    return value.toFixed(2);
  } else {
    return value ? "true" : "false";
  }
}

format("hello");    // "hello"
format(123.456);    // "123.46"
format(true);       // "true"


// より実践的な例
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型

「より実践的な例」では、引数によって戻り値の型を厳密にできる。

keyof演算子(型安全なプロパティアクセス)

keyofは、オブジェクトの型からプロパティ名(キー)の型を取得する演算子です。

演算結果として、返される型はUnion型(リテラル型のUnion) になります。

基本的な使い方

TypeScript
type User = {
  name: string;
  age: number;
  email: string;
};

// keyof User は 演算結果として"name" | "age" | "email" という”Union型”になる
type UserKeys = keyof User;

let key: UserKeys;
key = "name";   // OK
key = "age";    // OK
key = "email";  // OK
// key = "invalid";  // エラー!存在しないプロパティ

実用例:型安全なプロパティアクセス

TypeScript
function getValue(obj: User, key: keyof User) {
  return obj[key];
}

const user: User = {
  name: "太郎",
  age: 25,
  email: "taro@example.com"
};

getValue(user, "name");    // OK - string型が返る
getValue(user, "age");     // OK - number型が返る
// getValue(user, "xxx");  // エラー!存在しないプロパティ

数値インデックスや配列の場合

TypeScript
// 配列の場合
type ArrayKeys = keyof string[];  // number | "length" | "toString" | ... など

keyof演算子とtypeof演算子との組み合わせ

TypeScriptにはtypeof演算子が2つの意味で使われます:

1)JavaScriptのtypeof(実行時に値の型を調べる

TypeScript
console.log(typeof "hello");  // "string"
console.log(typeof 123);      // "number"

2)TypeScriptのtypeof値から型定義を取得する)

TypeScript
const user = {
  name: "太郎",
  age: 25
};

// userという”値”から型を取得
type UserType = typeof user;  // { name: string; age: number; }

keyofとtypeofを組み合わせる理由

値(オブジェクトリテラル)のキーを取得したい場合、keyoftypeofを組み合わせます。

  • typeofで値から型定義を取得
  • 型定義からkeyofで型→キーのUnion型を取得
TypeScript
const config = {
  apiKey: "abc123",
  timeout: 3000
} as const;

// ❌ エラー:configは値であって型ではない
// type ConfigKeys = keyof config;

// ✅ 正しい:typeofで値→型、keyofで型→キーのUnion型
type ConfigKeys = keyof typeof config;  // "apiKey" | "timeout"

// 動作の流れ:
// 1. typeof config → { readonly apiKey: "abc123"; readonly timeout: 3000; }
// 2. keyof → "apiKey" | "timeout"

実用例

5行目のようにas constを付けることでリテラル型になる事に注目してください。

TypeScript
const STATUS = {
  PENDING: "pending",
  PROCESSING: "processing",
  COMPLETED: "completed"
} as const;  // ← 重要:as constを付けることでリテラル型になる

// as constの効果を理解する
// as constなしの場合:
const STATUS_WITHOUT = {
  PENDING: "pending",
  PROCESSING: "processing"
};
type Value1 = typeof STATUS_WITHOUT["PENDING"];  // string(広い型)

// as constありの場合:
const STATUS_WITH = {
  PENDING: "pending",
  PROCESSING: "processing"
} as const;
type Value2 = typeof STATUS_WITH["PENDING"];  // "pending"(リテラル型)

// as constの効果:
// - オブジェクトのプロパティがreadonly(変更不可)になる
// - 値が具体的なリテラル型になる("pending"という固有の型)
// - stringではなく"pending"という厳密な型が得られる




// ステータス名の型(キー)
type StatusKey = keyof typeof STATUS;  // "PENDING" | "PROCESSING" | "COMPLETED"

// ステータス値の型(値)
type StatusValue = typeof STATUS[StatusKey];  // "pending" | "processing" | "completed"

// StatusValueの動作を段階的に理解する:
// 1. typeof STATUS → { readonly PENDING: "pending"; readonly PROCESSING: "processing"; ... }
// 2. typeof STATUS["PENDING"] → "pending"(as constのおかげでstringではなくリテラル型)
// 3. StatusKey = "PENDING" | "PROCESSING" | "COMPLETED"
// 4. typeof STATUS[StatusKey] = typeof STATUS["PENDING" | "PROCESSING" | "COMPLETED"]
//    → "pending" | "processing" | "completed"

// より分かりやすい例:
type PendingValue = typeof STATUS["PENDING"];      // "pending"(リテラル型)
type ProcessingValue = typeof STATUS["PROCESSING"];// "processing"(リテラル型)
type CompletedValue = typeof STATUS["COMPLETED"]; // "completed"(リテラル型)

// StatusKeyを使うと、これら全てのUnion型が得られる
// StatusValue = PendingValue | ProcessingValue | CompletedValue

// 使用例
function updateStatus(status: StatusValue) {
  console.log(`ステータスを${status}に更新`);
}

updateStatus("pending");      // OK
updateStatus(STATUS.PENDING); // OK
// updateStatus("invalid");   // エラー!
// updateStatus("string");    // エラー!as constのおかげで厳密にチェックされる

別の書き方:より理解しやすい方法

TypeScript
const STATUS = {
  PENDING: "pending",
  PROCESSING: "processing",
  COMPLETED: "completed"
} as const;

// 方法1: typeof STATUS[keyof typeof STATUS]
type StatusValue1 = typeof STATUS[keyof typeof STATUS];

// 方法2: より明示的(同じ結果)
type StatusObject = typeof STATUS;
type StatusValue2 = StatusObject[keyof StatusObject];

// 方法3: さらに分かりやすく(同じ結果)
type StatusValue3 = 
  | typeof STATUS["PENDING"]
  | typeof STATUS["PROCESSING"]
  | typeof STATUS["COMPLETED"];

// すべて "pending" | "processing" | "completed" という同じ型になる

ジェネリクス入門

ジェネリクス(Generics) は、「型をあとから決められる仕組み」 です。

ジェネリクスは、型を「引数」として受け取ることで、柔軟で再利用可能なコードを書く強力な機能です。

ジェネリクスが必要な理由

  • メソッドをオーバーロードするとなると引数や返り値の違う同名の関数を型ごと作る必要がある。
  • any型で一つ関数にまとめると型の安全性のメリットは失われる

どちらの問題も回避して一つの関数で関数の定義ができるのメリットがある。

TypeScript
// ジェネリクスを使わない場合
function getFirstString(arr: string[]): string {
  return arr[0];
}

function getFirstNumber(arr: number[]): number {
  return arr[0];
}

// 型ごとに関数を作る必要がある...面倒!

// anyを使うと型安全性が失われる
function getFirstAny(arr: any[]): any {
  return arr[0];
}

const result = getFirstAny([1, 2, 3]);  // any型になってしまう

ジェネリクスの基本

TypeScript
// <T>は型パラメータ(慣例的にTを使う)
function getFirst<T>(arr: T[]): T {
  return arr[0];
}

// 使用時に型を指定
const firstNumber = getFirst<number>([1, 2, 3]);     // number型
const firstName = getFirst<string>(["a", "b", "c"]); // string型

// 型推論も効く
const first = getFirst([1, 2, 3]);  // number型と推論される

一言でいうと👇

ジェネリクス = 型の変数

”T”が型の変数のような役割をする。 7、8行目のように<型>の部分に具体的なデータ型を指定した時点でT変数のデータ型が確定する。(それまでは、型が未定な状態のイメージ)

Tは変数名のようなもので、Tでなくても良い(AでもBでも良い。)

複数の型パラメータ

TypeScript
// 2つの型パラメータを持つ関数
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const p1 = pair<string, number>("age", 25);      // [string, number]
const p2 = pair("name", "太郎");                  // 型推論が効く
const p3 = pair<boolean, string[]>(true, ["a"]); // [boolean, string[]]

// 実用例:キーと値のペアを作る
function createEntry<K, V>(key: K, value: V): { key: K; value: V } {
  return { key, value };
}

const entry1 = createEntry("name", "太郎");
const entry2 = createEntry(1, { id: 100, name: "商品A" });

ジェネリック型エイリアス

サンプルコードでは4行目のTの型が、9,10行目のオブジェクト型に確定する事を示しています。

TypeScript
// レスポンスの型をジェネリックで定義
type ApiResponse<T> = {
  success: boolean;
  data: T;
  message: string;
};

// 具体的な型で使用
type UserResponse = ApiResponse<{ id: number; name: string }>;
type ProductResponse = ApiResponse<{ id: number; name: string; price: number }>;

// 使用例
const userResponse: UserResponse = {
  success: true,
  data: { id: 1, name: "太郎" },
  message: "ユーザー取得成功"
};

// 配列のレスポンス
type ListResponse<T> = {
  success: boolean;
  data: T[];
  total: number;
};

const productsResponse: ListResponse<{ id: number; name: string }> = {
  success: true,
  data: [
    { id: 1, name: "商品A" },
    { id: 2, name: "商品B" }
  ],
  total: 2
};

ジェネリックインターフェース

TypeScript
// ジェネリックなインターフェース
interface Box<T> {
  value: T;
  getValue: () => T;
  setValue: (value: T) => void;
}

// 具体的な型で使用
const numberBox: Box<number> = {
  value: 42,
  getValue() {
    return this.value;
  },
  setValue(value: number) {
    this.value = value;
  }
};

const stringBox: Box<string> = {
  value: "hello",
  getValue() {
    return this.value;
  },
  setValue(value: string) {
    this.value = value;
  }
};

制約付きジェネリクス

型パラメータに制約を加えることができます。

サンプルコードの例では、11〜14行目のうち14行目だけがNGなのは、唯一lengthプロパティを持っていないからです。

このように制約をコントロールできます。

TypeScript
// extendsで制約を追加
interface HasLength {
  length: number;
}

// TはHasLengthを持つ型でなければならない
function logLength<T extends HasLength>(item: T): void {
  console.log(item.length);
}

logLength("hello");        // OK(stringはlengthを持つ)
logLength([1, 2, 3]);      // OK(配列はlengthを持つ)
logLength({ length: 10 }); // OK
// logLength(123);         // エラー!numberはlengthを持たない

// より実践的な例
interface Identifiable {
  id: number;
}

function findById<T extends Identifiable>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

const users = [
  { id: 1, name: "太郎", email: "taro@example.com" },
  { id: 2, name: "花子", email: "hanako@example.com" }
];

const user = findById(users, 1);  // { id: number; name: string; email: string } | undefined

keyofとジェネリクスの組み合わせ

keyofとジェネリクスを組み合わせると、任意のオブジェクトに対して型安全な関数を作れます。

TypeScript
// keyofでオブジェクトのキーの型を取得
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = {
  name: "太郎",
  age: 25,
  email: "taro@example.com"
};

const name = getProperty(person, "name");    // string型
const age = getProperty(person, "age");      // number型
// const invalid = getProperty(person, "invalid");  // エラー!

// 実用例:オブジェクトの値を更新する関数
function updateProperty<T, K extends keyof T>(
  obj: T,
  key: K,
  value: T[K]
): void {
  obj[key] = value;
}

updateProperty(person, "age", 26);        // OK
// updateProperty(person, "age", "26");   // エラー!型が合わない

なぜkeyofとジェネリクスの組み合わせが強力か?

  • どんなオブジェクトでも使える汎用的な関数が作れる(ジェネリクスの効果)
  • プロパティ名の型チェックができる(ketofの効果)
  • 値の型も正しく推論される
  • 間違ったプロパティ名や型のミスをコンパイル時に検出できる

実践例:汎用的なデータフェッチ関数

学んだ知識を使って、実践的な関数を作ってみましょう。

TypeScript
// APIレスポンスの型
type ApiResponse<T> = {
  success: boolean;
  data: T;
  error?: string;
};

// フェッチ関数(ジェネリック)
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  try {
    const response = await fetch(url);
    const data: T = await response.json();
    return {
      success: true,
      data
    };
  } catch (error) {
    return {
      success: false,
      data: {} as T,
      error: error instanceof Error ? error.message : "Unknown error"
    };
  }
}

// ユーザーの型定義
type User = {
  id: number;
  name: string;
  email: string;
};

// 商品の型定義
type Product = {
  id: number;
  name: string;
  price: number;
};

// 使用例
async function loadUser() {
  const response = await fetchData<User>("/api/users/1");
  
  if (response.success) {
    console.log(response.data.name);  // string型
    console.log(response.data.email); // string型
  } else {
    console.error(response.error);
  }
}

async function loadProducts() {
  const response = await fetchData<Product[]>("/api/products");
  
  if (response.success) {
    response.data.forEach(product => {
      console.log(product.name, product.price);
    });
  }
}

ポイント

  • 12行目でレスポンスのデータ型がジェネリクスの恩恵を受けて、引数で私たデータ型に応じた型チェックをしてくれる(42行目の処理ではUser型に一致したレスポンス。53行目ではProduct型に一致したレスポンス)
  • メソッドの返り値の型も2行目の定義によって柔軟に変化しつつ型チェックはしっかりされる

実践例:汎用的なリストフィルター

TypeScript
// フィルター条件の型
type FilterCondition<T> = (item: T) => boolean;

// 汎用的なフィルター関数
function filterList<T>(items: T[], conditions: FilterCondition<T>[]): T[] {
  return items.filter(item => 
    conditions.every(condition => condition(item))
  );
}

// ユーザーリストをフィルター
type User = {
  id: number;
  name: string;
  age: number;
  isActive: boolean;
};

const users: User[] = [
  { id: 1, name: "太郎", age: 25, isActive: true },
  { id: 2, name: "花子", age: 30, isActive: false },
  { id: 3, name: "次郎", age: 35, isActive: true }
];

// フィルター条件を定義
const isAdult: FilterCondition<User> = (user) => user.age >= 20;
const isActive: FilterCondition<User> = (user) => user.isActive;

// 20歳以上でアクティブなユーザーを取得
const activeAdults = filterList(users, [isAdult, isActive]);
console.log(activeAdults);
// [{ id: 1, name: "太郎", age: 25, isActive: true },
//  { id: 3, name: "次郎", age: 35, isActive: true }]

ポイント

  • 26,27行目で2種類のフィルター条件を30行目でフィルターリストとして私ている。
  • 7行目の.every()で両方のフィルター条件に合致しないitemは6行目のfilterメソッドで除外される
  • もちろんUser型に一致しないデータ型を渡すと型チェックが機能する

実践例:型安全なイベントエミッター

TypeScript
// イベント名と引数の型のマッピング
type EventMap = {
  login: { userId: number; timestamp: Date };
  logout: { userId: number };
  purchase: { productId: number; amount: number };
};

// イベントハンドラーの型
type EventHandler<T> = (data: T) => void;

// 型安全なイベントエミッター
class TypedEventEmitter<T extends Record<string, any>> {
  private listeners: {
    [K in keyof T]?: EventHandler<T[K]>[];
  } = {};

  // イベントリスナーを登録
  on<K extends keyof T>(event: K, handler: EventHandler<T[K]>): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(handler);
  }

  // イベントを発火
  emit<K extends keyof T>(event: K, data: T[K]): void {
    const handlers = this.listeners[event];
    if (handlers) {
      handlers.forEach(handler => handler(data));
    }
  }
}

// 使用例
const emitter = new TypedEventEmitter<EventMap>();

// 型安全にリスナーを登録
emitter.on("login", (data) => {
  console.log(`ユーザー ${data.userId} がログインしました`);
  console.log(`時刻: ${data.timestamp}`);
});

emitter.on("purchase", (data) => {
  console.log(`商品 ${data.productId}${data.amount} 円で購入`);
});

// 型安全にイベントを発火
emitter.emit("login", { userId: 1, timestamp: new Date() });
emitter.emit("purchase", { productId: 100, amount: 5000 });

// エラーになる例
// emitter.emit("login", { userId: "1" });  // エラー!userIdはnumber
// emitter.emit("invalid", {});             // エラー!存在しないイベント

補足

TypedEventEmittet.listenersに格納されるデーターのイメージは、

listeners
 ├─ login    → [ handler1, handler2, handler3 ]
 ├─ logout   → [ handlerA ]
 └─ purchase → [ handlerX, handlerY ]

一つのイベントに複数のハンドラーが格納できるようにしているのは

例えば、「ユーザーがログインした」という1つの出来事に対して、やりたいことは複数あります。

  • 画面に「ログイン成功」を表示したい
  • ログイン履歴を保存したい
  • アクセス解析に送信したい
  • 管理者向け通知を送りたい

👉 全部「ログイン」という1イベントに複数の処理が反応している

TypeScriptの組み込みユーティリティ型

TypeScriptには、既存の型から新しい型を作るための便利なユーティリティ型が用意されています。

これらはジェネリクスを使って実装されており、型操作を簡単にしてくれます。

ユーティリティ型の一覧

ユーティリティ型説明使用例
Partial<T>全てオプショナルに変換
(英単語の’不全の', '部分的’をイメージする)
更新用の型
Required<T>全て必須設定の検証
Readonly<T>全て読み取り専用イミュータブルなデータ
Pick<T, K>特定のプロパティを選択
レスポンス用の型
Omit<T, K>特定のプロパティを除外
(英単語の‘省く’,’省略する‘をイメージする)
機密情報を除外
Record<K, T>キーと値の型を指定辞書型のオブジェクト

他にも`Extract`、`Exclude`、`NonNullable`、`ReturnType`などがありますが、上記の6つが最もよく使われます。

Partial<T> – 全てのプロパティをオプショナルに

全てのプロパティを省略可能にします。

TypeScript
type User = {
  id: number;
  name: string;
  email: string;
  age: number;
};

// 全てのプロパティがオプショナルになる
type PartialUser = Partial;
// 結果: { id?: number; name?: string; email?: string; age?: number; }

// 実用例:更新用の型
function updateUser(id: number, updates: Partial): void {
  // 一部のプロパティだけを更新できる
  console.log(`ユーザー${id}を更新:`, updates);
}

updateUser(1, { name: "太郎" });              // OK
updateUser(1, { email: "taro@example.com" }); // OK
updateUser(1, { name: "太郎", age: 26 });     // OK

Required<T> – 全てのプロパティを必須に

Partialの逆で、全てのプロパティを必須にします。

TypeScript
type PartialConfig = { 
  apiKey?: string; 
  timeout?: number; 
  retries?: number; 
};

// 全てのプロパティが必須になる
type RequiredConfig = Required;
// 結果: { apiKey: string; timeout: number; retries: number; }

const config: RequiredConfig = { 
  apiKey: "abc123", 
  timeout: 3000, 
  retries: 3 
  // 全て指定しないとエラー
};

Readonly<T> – 全てのプロパティを読み取り専用に

全てのプロパティを変更不可にします。

TypeScript
type User = { 
  id: number; 
  name: string; 
}; 
type ReadonlyUser = Readonly; 
// 結果: { readonly id: number; readonly name: string; }

const user: ReadonlyUser = { 
  id: 1, 
  name: "太郎" 
}; 
// user.name = "花子"; 
// エラー!readonlyは変更不可

Pick<T, K> – 特定のプロパティだけを選択

オブジェクト型から特定のプロパティだけを取り出します。

TypeScript
type User = { 
  id: number; 
  name: string; 
  email: string; 
  age: number; 
  password: string; 
}; 

// 例1: nameとemailだけを選択
type UserProfile = Pick<User, "name" | "email">;
//   Pick<User型から, nameとemailを選ぶ>
// これは以下と同じ意味
//type UserProfile = {name: string; email: string;};

// 例2: idだけを選択
type UserId = Pick<User, "id">;
// 結果: { id: number; }

// 例3: 3つのプロパティを選択
type UserBasicInfo = Pick<User, "id" | "name" | "age">;
// 結果: { id: number; name: string; age: number; }

// 実用例:APIレスポンス用の型(パスワードを含まない)
type UserResponse = Pick<User, "id" | "name" | "email" | "age">;

const response: UserResponse = {
  id: 1,
  name: "太郎",
  email: "taro@example.com",
  age: 25
  // passwordは含まれない(選択していないから)
};

Pickの使い方を覚えるコツ

TypeScript
Pick<どの型から, "何を" | "選ぶか"> 
     ^^^^^^^^. ^^^^^^^^^^^^^^^^ 
     元の型.    欲しいプロパティをUnion型で指定 

比喩で理解する

果物の詰め合わせ(User型)から、リンゴとバナナ(nameとemail)だけを選んで新しい箱(UserProfile型)に入れる、というイメージです。

TypeScript
// 元の詰め合わせ 
type FruitBox = {
  apple: string;
  banana: string;
  orange: string;
  grape: string;
};

// リンゴとバナナだけを選ぶ 
type MyFavorites = Pick<FruitBox, "apple" | "banana">;
// 結果: { apple: string; banana: string; }

Omit<T, K> – 特定のプロパティを除外

Pickので、特定のプロパティを除いた型を作ります。

TypeScript
type User = {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
};

// 例1: passwordを除外
type UserWithoutPassword = Omit<User, "password">;
//   Omit<User型から, passwordを除く>

// 結果: { id: number; name: string; email: string; createdAt: Date; }
// passwordだけがない(除外したから)


// 例2: 複数のプロパティを除外
type CreateUserDto = Omit<User, "id" | "createdAt">;
//   Omit<User型から, idとcreatedAtを除く>

// 結果: { name: string; email: string; password: string; }
// idとcreatedAtだけがない

const newUser: CreateUserDto = {
  name: "太郎",
  email: "taro@example.com",
  password: "secret123"
  // idとcreatedAtは不要(除外したから)
};


// 例3: パスワードと作成日時を除外
type UserPublicInfo = Omit<User, "password" | "createdAt">;
// 結果: { id: number; name: string; email: string; }

PickとOmitと実用的な使い分け

TypeScript
// プロパティが少ない→除外したいものが多い→Pick 
type UserResponse = Pick<User, "id" | "name" | "email">;
// プロパティが多い→除外したいものが少ない→Omit 
type UserResponse = Omit<User, "password">; // こっちの方が簡潔

Record<K, T> – キーと値の型を指定

キーと値の型を指定してオブジェクト型を作ります。

TypeScript
// 基本的な使い方
type ScoreRecord = Record<string, number>;
//   Record<キーはstring型, 値はnumber型>
//          ^^^^^^^^^^^^^  ^^^^^^^^^^^^^^
//          プロパティ名    プロパティの値

// これは以下と”同じ意味”
type ScoreRecord = {
  [key: string]: number;  // どんな文字列キーでもOK、値はnumber
};


const scores: ScoreRecord = {
  math: 90,        // キー"math"(string): 値90(number)
  english: 85,     // キー"english"(string): 値85(number)
  science: 92,     // キー"science"(string): 値92(number)
  history: 88      // どんなキーでも追加できる(stringならOK)
};

// 段階的に理解する
// Record<string, number> は
// 「キーがstring型、値がnumber型のオブジェクト」という意味

より厳密な例:特定のキーを指定

TypeScript
// 例1: 特定の文字列だけをキーにする
type Subject = "math" | "english" | "science";
type Scores = Record<Subject, number>;
//   Record<"math" | "english" | "science", number>
//          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^
//          キーはこの3つのみ                 値はnumber

const myScores: Scores = {
  math: 90,
  english: 85,
  science: 92
  // この3つが必須(他のキーは追加できない)
};

// const invalid: Scores = {
//   math: 90,
//   history: 88  
// エラー!historyは許可されていない
// 
};

// 例2: 成績表
type Grade = "A" | "B" | "C" | "D" | "F";
type Subject = "math" | "english" | "science";

type GradeBook = Record<Subject, Grade>;
//   Record<科目名の型, 成績の型>

const grades: GradeBook = {
  math: "A", 
  // mathキー: "A"という値
  english: "B",
  // englishキー: "B"という値
  science: "A"     
  // scienceキー: "A"という値
  // 3つの科目全てが必要
};

// const invalid: GradeBook = {
//   math: "A",
//   english: 100  // エラー!値はGrade型("A"|"B"|"C"|"D"|"F")でなければならない
// };

いつRecordを使うべき?

  • キーが的(実行時に決まる)
  • キーと値の型が明確
  • 辞書的なデータ構造
TypeScript
// 良い例 
type Cache = Record<string, any>; 
// キャッシュ 
type Config = Record<string, string>; 
// 設定 
type Lookup = Record<number, User>; 
// IDでユーザーを検索 

実践例:ユーティリティ型の組み合わせ

TypeScript
type User = {
  id: number;
  name: string;
  email: string;
  password: string;
  role: "admin" | "user";
  createdAt: Date;
  updatedAt: Date;
};

// 作成用の型:id、createdAt、updatedAtを除外
type CreateUserDto = Omit<User, "id" | "createdAt" | "updatedAt">;
// CreateUserDtoの中身はこうなる:
// {
//   name: string;
//   email: string;
//   password: string;
//   role: "admin" | "user";
// }

// 更新用の型:CreateUserDtoの全プロパティをオプショナルに
type UpdateUserDto = Partial<CreateUserDto>;
// UpdateUserDtoの中身はこうなる:
// {
//   name?: string;
//   email?: string;
//   password?: string;
//   role?: "admin" | "user";
// }

type UserResponse = Readonly<Omit<User, "password">>;
// UserResponseの中身はこうなる:
// {
//   readonly id: number;
//   readonly name: string;
//   readonly email: string;
//   readonly role: "admin" | "user";
//   readonly createdAt: Date;
//   readonly updatedAt: Date;
// }

// 使用例
function createUser(data: CreateUserDto): User {
  return {
    ...data,
    id: Date.now(),              // 自動生成
    createdAt: new Date(),       // 自動生成
    updatedAt: new Date()        // 自動生成
  };
}

function updateUser(id: number, data: UpdateUserDto): void {
  // 一部のプロパティだけを更新
  console.log(`ユーザー${id}を更新:`, data);
}

// 作成(4つのプロパティが必要)
const newUser = createUser({
  name: "太郎",
  email: "taro@example.com",
  password: "secret123",
  role: "user"
});

// 更新(一部だけでOK、全てオプショナルだから)
updateUser(1, { name: "山田太郎" });                   // nameだけ更新
updateUser(1, { email: "yamada@example.com" });      // emailだけ更新
updateUser(1, { name: "山田太郎", role: "admin" });    // 複数更新
updateUser(1, {});                                   // 何も更新しなくてもOK

// レスポンス(全て読み取り専用、passwordなし)
const response: UserResponse = {
  id: 1,
  name: "太郎",
  email: "taro@example.com",
  role: "user",
  createdAt: new Date(),
  updatedAt: new Date()
};

// response.name = "花子";  // エラー!Readonlyなので変更不可

ジェネリクスのベストプラクティス

✅ 良い例

TypeScript
// 1. 明確な命名(Tだけでなく意味のある名前も使える)
function createPair<TKey, TValue>(key: TKey, value: TValue) {
  return { key, value };
}

// 2. 適切な制約
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

// 3. デフォルト型パラメータ
type ApiResponse<T = unknown> = {
  data: T;
  status: number;
};

unknown 型は、「型がまだ分からない値」を安全に扱うための型です。

unknown = 安全版 any

型を確認すれば使えるようになります。(anyはTypeScriptの型チェックが無意味になります。)

❌ 避けるべき例

TypeScript
// 1. 不必要なジェネリクス
function bad<T>(value: T): T {
  return value;  // 単に値を返すだけならジェネリクス不要
}

// 2. 制約のないany的な使い方
function veryBad<T>(value: T): any {
  return value;  // 戻り値がanyになって型安全性が失われる
}

// 3. 過度に複雑なジェネリクス
type TooComplex<T, U, V, W, X, Y, Z> = ...;  // 読みにくい

よくある質問

Q1: ジェネリクスとanyの違いは?

TypeScript
// anyは型情報が失われる
function identityAny(value: any): any {
  return value;
}

const result1 = identityAny(123);
// result1はany型なので、どんなメソッドでも呼べてしまう(危険)
result1.toUpperCase();  // エラーにならないが実行時に失敗する

// ジェネリクスは型情報が保持される
function identity<T>(value: T): T {
  return value;
}

const result2 = identity(123);
// result2はnumber型として推論される
// result2.toUpperCase();  // コンパイルエラー!事前に問題を見つけることができる。

Q2: いつジェネリクスを使うべき?

ジェネリクスを使うべき場合:

  • 複数の型で同じロジックを使いたい
  • 型安全性を保ちながら柔軟性が必要
  • 配列操作、データ構造、ユーティリティ関数など

ジェネリクスが不要な場合:

  • 特定の型でしか使わない関数
  • シンプルな値の変換
  • 型が明確で変わらない場合

まとめ

今回は、関数の型定義とジェネリクスについて学びました。

学んだこと

  • 関数の基本的な型定義
  • オプショナルパラメータ、デフォルトパラメータ、Restパラメータ
  • 関数型と関数のオーバーロード
  • ジェネリクスの基本概念
  • 制約付きジェネリクス
  • keyofとジェネリクスの組み合わせ
  • 実践的なジェネリクスの使用例

次回予告 第4回では、クラスとオブジェクト指向、より高度な型操作について学びます。

TypeScript入門シリーズ

第1回: 環境構築から全体像まで
第2回: 型の基礎を深く理解する
第3回: 関数と型、ジェネリクス入門
第4回: インターフェースとクラス
第5回: 実践編 – 実際のプロジェクトでの活用