メインコンテンツまでスキップ

オブジェクト型・型エイリアス

この章では、オブジェクト型の定義方法と、型エイリアス(type)を使った再利用可能な型の作成方法を学びます。

この章で学ぶこと

  • オブジェクト型の定義方法
  • 型エイリアス(type)の使い方
  • オプショナルプロパティ(?)
  • readonly修飾子
  • インデックスシグネチャ
備考

この章の内容は、実際のアプリケーション開発で頻繁に使用する重要な概念です。オブジェクトの型を適切に定義することで、型安全なコードを書けるようになります。

オブジェクト型の基本

オブジェクト型は、プロパティとその型を定義します。

オブジェクトの型注釈

// オブジェクトの型注釈
let person: {
name: string;
age: number;
} = {
name: 'Alice',
age: 25
};

// プロパティへのアクセス
console.log(person.name); // 'Alice'
console.log(person.age); // 25

// プロパティの変更
person.age = 26; // OK

// 存在しないプロパティへのアクセスはエラー
// person.email = 'alice@example.com'; // Error: プロパティ'email'は型に存在しない

型推論を活用

// 型推論に任せる(推奨)
let person = {
name: 'Alice',
age: 25
};
// person の型: { name: string; age: number; }

// 型推論された後も、プロパティの追加はできない
// person.email = 'alice@example.com'; // Error

型エイリアス(Type Alias)

複雑な型に名前を付けて再利用可能にする機能です。

基本的な型エイリアス

// 基本的な構文: type 型名 = 型;
type UserID = number;
type UserName = string;

let id: UserID = 123;
let name: UserName = 'Alice';

オブジェクト型のエイリアス

// オブジェクトの型に名前を付ける
type User = {
id: number;
name: string;
email: string;
age: number;
};

let user1: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
age: 25
};

let user2: User = {
id: 2,
name: 'Bob',
email: 'bob@example.com',
age: 30
};

Union型のエイリアス

// よく使うUnion型に名前を付ける
type Status = 'pending' | 'approved' | 'rejected';
type ID = string | number;

let orderStatus: Status = 'pending';
let userId: ID = 123;
let productId: ID = 'ABC-001';

複雑な型のエイリアス

// 複雑なオブジェクト型を組み合わせる
type Address = {
zipCode: string;
prefecture: string;
city: string;
street: string;
building?: string; // オプショナル
};

type Contact = {
email: string;
phone?: string; // オプショナル
};

type Customer = {
id: number;
name: string;
address: Address;
contact: Contact;
registeredAt: Date;
};

let customer: Customer = {
id: 1,
name: '田中太郎',
address: {
zipCode: '100-0001',
prefecture: '東京都',
city: '千代田区',
street: '千代田1-1-1'
},
contact: {
email: 'tanaka@example.com',
phone: '03-1234-5678'
},
registeredAt: new Date()
};

関数型のエイリアス

// 関数の型にエイリアスを付ける
type MathOperation = (a: number, b: number) => number;

const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
const multiply: MathOperation = (a, b) => a * b;
const divide: MathOperation = (a, b) => a / b;

console.log(add(10, 5)); // 15
console.log(subtract(10, 5)); // 5
console.log(multiply(10, 5)); // 50
console.log(divide(10, 5)); // 2

// コールバック関数の型
type Callback = (result: string) => void;
type Validator = (value: string) => boolean;

オプショナルプロパティ

プロパティ名の後に?を付けると、そのプロパティは省略可能になります。

// オプショナルプロパティ(?を付ける)
type User = {
name: string;
age: number;
email?: string; // 省略可能
};

// emailを省略
let user1: User = {
name: 'Bob',
age: 30
};

// emailを指定
let user2: User = {
name: 'Charlie',
age: 35,
email: 'charlie@example.com'
};

// オプショナルプロパティにアクセス
console.log(user1.email); // undefined
console.log(user2.email); // 'charlie@example.com'

オプショナルプロパティの安全な使用

type User = {
name: string;
email?: string;
};

let user: User = { name: 'Alice' };

// オプショナルプロパティを使う際は存在チェックが必要
if (user.email) {
console.log(user.email.toUpperCase()); // OK
}

// Optional Chaining(?.演算子)で安全にアクセス
console.log(user.email?.toUpperCase()); // undefined(エラーにならない)

// Nullish Coalescing(??演算子)でデフォルト値を設定
let email = user.email ?? 'no-email@example.com';
console.log(email); // 'no-email@example.com'

readonly修飾子

readonlyを付けると、プロパティが読み取り専用になります。

// readonlyプロパティ
type Config = {
readonly apiUrl: string;
readonly timeout: number;
retries: number; // 通常のプロパティ
};

let config: Config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};

// 読み取りはOK
console.log(config.apiUrl); // 'https://api.example.com'
console.log(config.timeout); // 5000

// 通常のプロパティは変更できる
config.retries = 5; // OK

// readonlyプロパティは変更できない
// config.apiUrl = 'https://api2.example.com'; // Error
// config.timeout = 3000; // Error

実践的なreadonly使用例

// APIのレスポンス型(変更不可)
type ApiResponse = {
readonly status: number;
readonly data: {
readonly id: number;
readonly name: string;
};
};

// 定数オブジェクト
type AppConstants = {
readonly APP_NAME: string;
readonly VERSION: string;
readonly MAX_RETRIES: number;
};

const APP_CONSTANTS: AppConstants = {
APP_NAME: 'MyApp',
VERSION: '1.0.0',
MAX_RETRIES: 3
};

// APP_CONSTANTS.APP_NAME = 'NewApp'; // Error

Readonly<T>ユーティリティ型

すべてのプロパティを一括でreadonlyにする場合は、Readonly<T>を使います。

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

// すべてのプロパティがreadonlyになる
type ReadonlyUser = Readonly<User>;
// {
// readonly id: number;
// readonly name: string;
// readonly email: string;
// }

let user: ReadonlyUser = {
id: 1,
name: 'Alice',
email: 'alice@example.com'
};

// user.name = 'Bob'; // Error: readonly

インデックスシグネチャ

プロパティ名が動的な場合や、多数のプロパティがある場合に使用します。

基本的なインデックスシグネチャ

// インデックスシグネチャの基本構文
type Dictionary = {
[key: string]: string;
};

let dictionary: Dictionary = {
apple: 'りんご',
banana: 'バナナ',
orange: 'オレンジ'
};

// 任意のキーでアクセス可能
console.log(dictionary.apple); // 'りんご'
console.log(dictionary['banana']); // 'バナナ'

// 新しいプロパティを追加
dictionary.grape = 'ぶどう'; // OK
dictionary['melon'] = 'メロン'; // OK

実践的な使用例

// 例1: スコアボード
type Scores = {
[playerName: string]: number;
};

let scores: Scores = {
'Alice': 100,
'Bob': 85,
'Charlie': 92
};

scores['David'] = 88; // OK

// 例2: 設定オブジェクト
type Settings = {
[key: string]: string | number | boolean;
};

let settings: Settings = {
theme: 'dark',
fontSize: 14,
autoSave: true,
language: 'ja'
};

// 例3: APIレスポンスのヘッダー
type Headers = {
[headerName: string]: string;
};

let headers: Headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
'Accept-Language': 'ja-JP'
};

インデックスシグネチャと固定プロパティの組み合わせ

// 固定プロパティとインデックスシグネチャを組み合わせる
type User = {
id: number; // 必須プロパティ
name: string; // 必須プロパティ
[key: string]: string | number; // その他の任意のプロパティ
};

let user: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com', // OK(インデックスシグネチャで許可)
age: 25 // OK
};
警告

インデックスシグネチャの型と固定プロパティの型は互換性が必要です。

// エラーになる例
type Invalid = {
name: string;
[key: string]: number; // Error: nameはstring型だがインデックスシグネチャはnumber型
};

// 修正: Union型を使う
type Valid = {
name: string;
[key: string]: string | number; // OK
};

型エイリアスの組み合わせ

Intersection型(型の交差)

複数の型を合成して、すべてのプロパティを持つ型を作成します。

// 既存の型エイリアスを組み合わせる
type Point = {
x: number;
y: number;
};

type Color = {
r: number;
g: number;
b: number;
};

// Intersection型で組み合わせる
type ColoredPoint = Point & Color;

let point: ColoredPoint = {
x: 10,
y: 20,
r: 255,
g: 0,
b: 0
};

実践的な型設計

// 基本的なエンティティ型
type BaseEntity = {
id: number;
createdAt: Date;
updatedAt: Date;
};

// ユーザー固有のプロパティ
type UserData = {
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
};

// 組み合わせてUser型を作成
type User = BaseEntity & UserData;

let user: User = {
id: 1,
createdAt: new Date(),
updatedAt: new Date(),
name: 'Alice',
email: 'alice@example.com',
role: 'admin'
};

// 商品型も同様に作成
type ProductData = {
name: string;
price: number;
stock: number;
};

type Product = BaseEntity & ProductData;

試してみよう: 商品データの型を設計しよう ★★

ECサイトの商品データを表す型を設計してください。

要件:

  1. Product型を定義

    • id: 数値(読み取り専用)
    • name: 文字列
    • price: 数値
    • description: 文字列(オプショナル)
    • category: 'electronics' | 'clothing' | 'food' | 'other'
    • inStock: 真偽値
    • tags: 文字列の配列(オプショナル)
  2. CartItem型を定義

    • product: Product型
    • quantity: 数値
  3. calculateTotal関数を作成

    • CartItem配列を受け取り、合計金額を返す
ヒント
  1. Product型はtype Product = { ... }で定義
  2. readonly修飾子をidに付ける
  3. オプショナルプロパティには?を付ける
  4. calculateTotal関数はreduceメソッドで合計を計算
回答と解説
// 商品カテゴリの型
type ProductCategory = 'electronics' | 'clothing' | 'food' | 'other';

// 商品の型定義
type Product = {
readonly id: number; // 読み取り専用
name: string;
price: number;
description?: string; // オプショナル
category: ProductCategory;
inStock: boolean;
tags?: string[]; // オプショナル
};

// カートアイテムの型定義
type CartItem = {
product: Product;
quantity: number;
};

// 合計金額を計算する関数
function calculateTotal(items: CartItem[]): number {
return items.reduce((total, item) => {
return total + (item.product.price * item.quantity);
}, 0);
}

// テストデータ
const laptop: Product = {
id: 1,
name: 'ノートパソコン',
price: 89800,
description: '高性能ノートPC',
category: 'electronics',
inStock: true,
tags: ['PC', 'ビジネス']
};

const tshirt: Product = {
id: 2,
name: 'Tシャツ',
price: 2980,
category: 'clothing',
inStock: true
// descriptionとtagsは省略可能
};

const snack: Product = {
id: 3,
name: 'ポテトチップス',
price: 198,
category: 'food',
inStock: false
};

// カートアイテム
const cartItems: CartItem[] = [
{ product: laptop, quantity: 1 },
{ product: tshirt, quantity: 2 },
{ product: snack, quantity: 5 }
];

// 合計金額を計算
const total = calculateTotal(cartItems);
console.log(`合計金額: ${total.toLocaleString()}`);
// 出力: 合計金額: 96,750円
// (89800 * 1) + (2980 * 2) + (198 * 5) = 89800 + 5960 + 990 = 96750

// 商品情報の表示
function displayProduct(product: Product): void {
console.log(`商品名: ${product.name}`);
console.log(`価格: ${product.price.toLocaleString()}`);
console.log(`カテゴリ: ${product.category}`);
console.log(`在庫: ${product.inStock ? 'あり' : 'なし'}`);

// オプショナルプロパティは存在チェック
if (product.description) {
console.log(`説明: ${product.description}`);
}
if (product.tags && product.tags.length > 0) {
console.log(`タグ: ${product.tags.join(', ')}`);
}
console.log('---');
}

displayProduct(laptop);
displayProduct(tshirt);
displayProduct(snack);

解説:

  • Product型では、readonly修飾子でidを変更不可にしています
  • descriptiontags?を付けてオプショナルにしています
  • ProductCategoryを別の型として定義することで、再利用性を高めています
  • calculateTotal関数はreduceメソッドで配列を集約し、合計金額を計算しています
  • オプショナルプロパティを使用する際は、存在チェックまたはOptional Chaining(?.)を使います

型の拡張例:

// 割引情報を追加した型
type DiscountedProduct = Product & {
discountRate: number; // 割引率(0.1 = 10%)
originalPrice: number;
};

// セール商品
const saleItem: DiscountedProduct = {
id: 4,
name: 'セール商品',
price: 8980, // 割引後価格
originalPrice: 9980,
discountRate: 0.1,
category: 'electronics',
inStock: true
};

まとめ

この章で学んだこと:

  • オブジェクト型: プロパティとその型を定義する
  • 型エイリアス(type): 複雑な型に名前を付けて再利用
  • オプショナルプロパティ(?): 省略可能なプロパティを定義
  • readonly修飾子: 変更不可のプロパティを定義
  • インデックスシグネチャ: 動的なプロパティ名に対応
  • Intersection型(&): 複数の型を組み合わせる

重要なポイント:

  • 型エイリアスを使って、再利用可能な型を定義しよう
  • オプショナルプロパティと必須プロパティを適切に使い分けよう
  • readonlyを使って、変更されるべきでないデータを保護しよう
  • 小さな型を組み合わせて、複雑な型を構築しよう

次の章では、any型やunknown型などの特殊な型について学びます。

初学者がつまずきやすいポイント

警告

よくある間違い

❌ オプショナルプロパティの存在チェックを忘れる

type User = {
name: string;
email?: string;
};

const user: User = { name: 'Alice' };

// ❌ 間違い: undefinedの可能性を考慮していない
console.log(user.email.toUpperCase());
// Error: 'user.email' is possibly 'undefined'

// ✅ 正しい: 存在チェックまたはOptional Chainingを使用
console.log(user.email?.toUpperCase()); // undefined(エラーにならない)

if (user.email) {
console.log(user.email.toUpperCase()); // OK
}

原因: オプショナルプロパティ(?付き)はundefinedの可能性があります。

解決策: Optional Chaining(?.)または条件分岐で存在チェックしてください。

❌ readonlyプロパティを変更しようとする

type Config = {
readonly apiUrl: string;
timeout: number;
};

const config: Config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};

// ❌ 間違い: readonlyプロパティを変更しようとする
config.apiUrl = 'https://new-api.example.com';
// Error: Cannot assign to 'apiUrl' because it is a read-only property

// ✅ 正しい: 新しいオブジェクトを作成
const newConfig: Config = {
...config,
apiUrl: 'https://new-api.example.com' // OK: スプレッドで新しいオブジェクトを構築している
};
// readonly は「既存プロパティへの再代入」のみ禁止する。新しいオブジェクトの構築は妨げない

原因: readonly修飾子は変更を禁止します。

解決策: 変更が必要な場合は新しいオブジェクトを作成するか、readonlyを外してください。

❌ インデックスシグネチャと固定プロパティの型不一致

// ❌ 間違い: 固定プロパティの型がインデックスシグネチャと互換性がない
type Invalid = {
name: string; // string型
[key: string]: number; // Error: 'name'はnumberでなければならない
};

// ✅ 正しい: Union型で両方の型を許可
type Valid = {
name: string;
[key: string]: string | number; // nameの型(string)を含むUnion型
};

原因: インデックスシグネチャは、そのオブジェクトのすべてのプロパティの型と互換性が必要です。

解決策: インデックスシグネチャの型を、固定プロパティの型を含むUnion型にしてください。

❌ 型エイリアスと変数を混同する

// 型エイリアス(型の定義)
type User = {
name: string;
age: number;
};

// ❌ 間違い: 型エイリアスを値として使おうとする
console.log(User); // Error: 'User' only refers to a type

// ✅ 正しい: 型エイリアスは型注釈として使う
const user: User = { name: 'Alice', age: 25 };
console.log(user); // OK

原因: 型エイリアスはコンパイル時のみ存在し、実行時には消えます。

解決策: 型エイリアスは変数の型注釈として使用してください。

❌ Intersection型でプロパティの競合

type A = {
value: string;
};

type B = {
value: number;
};

// Intersection型でプロパティの型が競合
type AB = A & B;
// valueの型は string & number = never(実質使用不可)

// ❌ 間違い: neverになったプロパティに値を代入できない
const ab: AB = {
value: 'hello' // Error: string は never に代入できない
};

// ✅ 正しい: 競合しないプロパティ名を使う
type A2 = { stringValue: string };
type B2 = { numberValue: number };
type AB2 = A2 & B2;

const ab2: AB2 = {
stringValue: 'hello',
numberValue: 42
};

原因: 同じプロパティ名で異なる型を持つ型をIntersectionすると、never型になります。

解決策: プロパティ名の競合を避けるか、設計を見直してください。


次に読む

特殊な型 へ進みます。any や unknown など、TypeScript の特殊な型の挙動と使い分けを学びます。