TypeScript レッスン4
ジェネリクス
型を引数にして、柔軟で再利用可能なコードを書こう
ジェネリクスとは?
ジェネリクス(Generics)は、 型を「引数」として受け取る仕組みです。関数やクラスを作るとき、 使う型を後から指定できるようになります。これにより、型安全を保ちつつ再利用可能なコードが書けます。
例えば「配列の最初の要素を返す関数」を考えてみましょう。 数値の配列にも文字列の配列にも使いたいですが、型を固定すると再利用できません。 ジェネリクスを使えば、入力の型に応じて戻り値の型も自動的に決まります。
// ジェネリクスなしの場合: 型ごとに関数が必要
function firstNumber(arr: number[]): number | undefined {
return arr[0];
}
function firstString(arr: string[]): string | undefined {
return arr[0];
}
// any を使うと型安全が失われる
function firstAny(arr: any[]): any {
return arr[0]; // 戻り値が any... 意味がない
}
// ジェネリクスを使う: <T> が型引数
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// 使用時に型が決まる
const num = first([1, 2, 3]); // num: number | undefined
const str = first(["a", "b", "c"]); // str: string | undefined
const bool = first([true, false]); // bool: boolean | undefined
// 明示的に型を指定することもできる
const explicit = first<string>(["hello"]);ジェネリック関数
複数の型引数を使ったり、型推論を活用したりできます。
// 複数の型引数
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const p = pair("hello", 42); // p: [string, number]
// 型引数を使った変換関数
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
const lengths = map(["hello", "world"], s => s.length);
// lengths: number[]
// アロー関数のジェネリクス
const identity = <T,>(value: T): T => value;
// ジェネリクスで型安全なイベントハンドラ
function createHandler<T>(initial: T) {
let value = initial;
return {
get: (): T => value,
set: (newValue: T): void => { value = newValue; },
};
}
const counter = createHandler(0); // T = number
counter.set(10); // OK
// counter.set("hello"); // Error: string は number に割り当てられない
const message = createHandler(""); // T = string
message.set("TypeScript"); // OK
// message.set(42); // Errorジェネリック的パターンを試す
ジェネリクスの概念をJavaScriptで体験しましょう。型に依存しない汎用関数を作ります。
Monaco + Shiki / Tab でインデント / Ctrl(Cmd)+Enter で再実行
index.htmllazy
1 lines0 issues
Monaco Editor を準備しています
表示領域に入った時点で Monaco と Shiki を初期化します。
preview.local
Live PreviewConsole
console.log / warn / error の出力がここに表示されます。
ジェネリック制約(extends)
型引数に制約をつけることで、「少なくともこのプロパティを持つ型」のように限定できます。
// T は { length: number } を持つ型に限定
function logLength<T extends { length: number }>(value: T): T {
console.log("長さ: " + value.length);
return value;
}
logLength("hello"); // OK: string は length を持つ
logLength([1, 2, 3]); // OK: 配列は length を持つ
logLength({ length: 10 }); // OK
// logLength(42); // Error: number に length はない
// キーの制約: T のキーに限定
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "太郎", age: 25, email: "taro@test.com" };
const name = getProperty(person, "name"); // string
const age = getProperty(person, "age"); // number
// getProperty(person, "address"); // Error: "address" は keyof person にない
// 条件型(Conditional Types)
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
// infer で型を抽出
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Result = ReturnType<() => string>; // stringジェネリックインターフェース
interfaceにも型引数を使えます。APIレスポンスやコレクションなど、汎用的なデータ構造に便利です。
// APIレスポンスの汎用型
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: Date;
}
// 使用例
interface User {
id: number;
name: string;
}
interface Product {
id: number;
title: string;
price: number;
}
// T に User を指定
const userRes: ApiResponse<User> = {
data: { id: 1, name: "太郎" },
status: 200,
message: "OK",
timestamp: new Date(),
};
// T に Product[] を指定
const productsRes: ApiResponse<Product[]> = {
data: [
{ id: 1, title: "りんご", price: 100 },
{ id: 2, title: "バナナ", price: 200 },
],
status: 200,
message: "OK",
timestamp: new Date(),
};
// ページネーション付きレスポンス
interface PaginatedResponse<T> extends ApiResponse<T[]> {
page: number;
totalPages: number;
totalItems: number;
}ユーティリティ型
TypeScriptには、よく使う型変換をまとめたユーティリティ型が 標準で用意されています。ジェネリクスを使って実装されています。
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Partial<T> — 全プロパティをオプショナルに
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number; }
// 更新時に便利: 変更したいフィールドだけ渡せる
function updateUser(id: number, updates: Partial<User>) { /* ... */ }
updateUser(1, { name: "太郎" }); // OK
// Required<T> — 全プロパティを必須に
type RequiredUser = Required<PartialUser>;
// { id: number; name: string; email: string; age: number; }
// Pick<T, K> — 指定したプロパティだけ抽出
type UserSummary = Pick<User, "id" | "name">;
// { id: number; name: string; }
// Omit<T, K> — 指定したプロパティを除外
type UserWithoutEmail = Omit<User, "email">;
// { id: number; name: string; age: number; }
// Record<K, V> — キーと値の型を指定
type UserRoles = Record<string, "admin" | "user" | "guest">;
const roles: UserRoles = {
taro: "admin",
hanako: "user",
};
// Readonly<T> — 全プロパティをreadonlyに
type ReadonlyUser = Readonly<User>;
// const user: ReadonlyUser = { ... };
// user.name = "変更"; // Error!
// Exclude, Extract — ユニオン型の操作
type Status = "active" | "inactive" | "pending" | "deleted";
type ActiveStatus = Exclude<Status, "deleted">;
// "active" | "inactive" | "pending"
type RemovedStatus = Extract<Status, "deleted" | "inactive">;
// "inactive" | "deleted"
// NonNullable — null, undefined を除外
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // stringユーティリティ型的パターンを実践
Partial、Pick、Omitなどの考え方をJavaScriptで体験しましょう。
Monaco + Shiki / Tab でインデント / Ctrl(Cmd)+Enter で再実行
index.htmllazy
1 lines0 issues
Monaco Editor を準備しています
表示領域に入った時点で Monaco と Shiki を初期化します。
preview.local
Live PreviewConsole
console.log / warn / error の出力がここに表示されます。
まとめ
<T>で型引数を定義し、再利用可能なコードを作るextendsでジェネリック制約を追加keyofでオブジェクトのキーを型として取得- interfaceやクラスにもジェネリクスを使える
Partial,Required,Pick,Omitなど標準ユーティリティ型を活用Record,Readonly,NonNullableも頻出