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

TypeScript入門 #2:型の基礎を深く理解する

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

TypeScript入門 #2:型の基礎を深く理解する

第1回では、TypeScriptの全体像と基本的な型を学びました。


TypeScriptを学ぶ上で欠かせない「型」の理解を一歩進めたい方へ──。

TypeScript入門 第2回:「型の基礎を深く理解する」は、前回の基本型の紹介からステップアップし、プリミティブ型や配列、オブジェクト型などの詳細な仕組みと、「実践でどう使うのか」を具体例を交えて丁寧に解説します。

実務での活用にも役立つヒント満載で、TypeScriptの型安全性や表現力をしっかり身につけられる内容です。

プリミティブ型の詳細

string型

文字列を扱う最も基本的な型です。

TypeScript
let message: string = "Hello, TypeScript!";
let name: string = '太郎';  // シングルクォートもOK

// テンプレートリテラル
let greeting: string = `こんにちは、${name}さん`;

// 複数行の文字列
let multiLine: string = `
  これは
  複数行の
  文字列です
`;

リテラル型:特定の文字列だけを許可

TypeScript
// "success", "error", "warning"のいずれかのみ許可
let status: "success" | "error" | "warning";

status = "success";  // OK
status = "error";    // OK
// status = "info";  // エラー!指定された値以外は不可

// 実用例:APIのレスポンスステータス
type ResponseStatus = "success" | "error" | "pending";

function handleResponse(status: ResponseStatus): void {
  if (status === "success") {
    console.log("成功しました");
  } else if (status === "error") {
    console.log("エラーが発生しました");
  }
}

number型

数値を扱う型です。

整数、小数、負の数、すべて同じnumber型です。

TypeScript
let integer: number = 42;
let float: number = 3.14;
let negative: number = -10;
let hex: number = 0xff;      // 16進数
let binary: number = 0b1010;  // 2進数
let octal: number = 0o744;    // 8進数

// 特殊な数値
let infinity: number = Infinity;
let notANumber: number = NaN;

Infinity とは、
「正の無限大」を表す特別な数値です。

NaN(Not a Number) とは、
「数値として計算できない結果」 を表す 特別な値です。

意味
Infinity無限大number
-Infinity負の無限大number
NaN数値にならないnumber

数値リテラル型

TypeScript
// 特定の数値のみ許可
let dice: 1 | 2 | 3 | 4 | 5 | 6;

dice = 3;  // OK
// dice = 7;  // エラー!

// 実用例:HTTP ステータスコード
type HttpStatus = 200 | 201 | 400 | 401 | 404 | 500;

function handleHttpStatus(status: HttpStatus): string {
  switch (status) {
    case 200:
      return "OK";
    case 404:
      return "Not Found";
    case 500:
      return "Internal Server Error";
    default:
      return "Unknown Status";
  }
}

boolean型

true/falseの真偽値を扱います。

TypeScript
let isActive: boolean = true;
let hasPermission: boolean = false;

// 条件式の結果も boolean
let isAdult: boolean = age >= 18;
let canVote: boolean = isAdult && hasPermission;

booleanリテラル型

TypeScript
// trueのみ許可
let alwaysTrue: true = true;
// alwaysTrue = false;  // エラー!

// 実用例:機能フラグ
type FeatureFlag = {
  darkMode: boolean;
  betaFeatures: true;  // ベータ機能は常にtrue
};

null と undefined

JavaScriptには「値がない」を表す2つの型があります。

TypeScript
let empty: null = null;
let notAssigned: undefined = undefined;

// 実務では Union型と組み合わせることが多い
let nullableString: string | null = null;
nullableString = "値を設定";
nullableString = null;  // OK

let optionalNumber: number | undefined = undefined;
optionalNumber = 42;
optionalNumber = undefined;  // OK

strictNullChecksオプション

tsconfig.jsonstrictNullChecks: trueにすると、nullとundefinedのチェックが厳密になります(推奨)。

TypeScript
// strictNullChecks: true の場合
let name: string = "太郎";
// name = null;  // エラー!

// nullを許可したい場合は明示的に
let nullableName: string | null = "太郎";
nullableName = null;  // OK

symbol と bigint

あまり使わないかもしれませんが、覚えておきましょう。

TypeScript
// symbol:一意な識別子
let sym1: symbol = Symbol("key");
let sym2: symbol = Symbol("key");
console.log(sym1 === sym2);  // false(それぞれユニーク)

// bigint:巨大な整数(ES2020以降)
let big: bigint = 100n;
let huge: bigint = BigInt("9007199254740991");

配列の詳細

基本的な配列の型定義

TypeScript
// 方法1:型[]
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["太郎", "花子", "次郎"];

// 方法2:Array<型>(ジェネリック記法)
let colors: Array<string> = ["red", "blue", "green"];
let scores: Array<number> = [80, 90, 75];

どちらも同じ意味ですが、number[]の方がシンプルで読みやすいため、こちらが推奨されます。

多次元配列

TypeScript
// 2次元配列
let matrix: number[][] = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

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

// 実用例:座標の配列
let coordinates: [number, number][] = [
  [10, 20],
  [30, 40],
  [50, 60]
];

タプル(Tuple):固定長で型が異なる配列

タプル(Tuple) とは、

👉 「要素数と順番ごとに型が決まっている配列」 です。

配列の各要素に異なる型を指定できます。

TypeScript
// [文字列, 数値]のペア
let user: [string, number] = ["太郎", 25];

// [ID, 名前, アクティブ状態]
let record: [number, string, boolean] = [1, "商品A", true];

// 実用例:座標
let point: [number, number] = [10, 20];
let point3D: [number, number, number] = [10, 20, 30];

// 実用例:関数の戻り値(エラーとデータのペア)
function fetchData(): [Error | null, string | null] {
  try {
    return [null, "データ取得成功"];
  } catch (error) {
    return [new Error("エラー"), null];
  }
}

const [error, data] = fetchData();
if (error) {
  console.error(error);
} else {
  console.log(data);
}

タプルのオプショナル要素とrest要素

オプショナル要素(optional element) とは、

👉 「あってもなくてもいい要素・プロパティ」 を表す仕組みです。

rest要素(Rest Element / Rest Parameters) は、

👉 「残りの値をひとまとめにして受け取る仕組み」 です。…(三点リーダ)を使います。

TypeScript
// オプショナル要素(?を付ける)
let optional: [string, number?] = ["太郎"];
optional = ["花子", 30];  // 両方OK

// rest要素(可変長)
let scores: [string, ...number[]] = ["太郎", 80, 90, 75];

readonly配列

変更不可能な配列を定義できます。

TypeScript
// 読み取り専用配列
let readonlyNumbers: readonly number[] = [1, 2, 3];
// readonlyNumbers.push(4);  // エラー!変更不可
// readonlyNumbers[0] = 10;  // エラー!要素の変更も不可

// ReadonlyArray<T>も同じ意味
let readonlyNames: ReadonlyArray<string> = ["太郎", "花子"];

配列の型推論

型推論(type inference) とは、

👉 プログラマが型を書かなくても、言語やコンパイラが自動で型を判断してくれる仕組みです。

TypeScript
// TypeScriptは初期値から型を推論する
let numbers = [1, 2, 3];  // number[]と推論
let mixed = [1, "two", 3];  // (number | string)[]と推論

// 空配列の場合は注意
let empty = [];  // any[]と推論されてしまう(避けるべき)
let emptyNumbers: number[] = [];  // 型を明示するべき

オブジェクトの詳細

基本的なオブジェクト型

TypeScript
// インライン型定義
let user: { name: string; age: number } = {
  name: "太郎",
  age: 25
};

// オブジェクトのネスト
let person: {
  name: string;
  age: number;
  address: {
    zip: string;
    city: string;
  };
} = {
  name: "花子",
  age: 30,
  address: {
    zip: "100-0001",
    city: "東京都"
  }
};

オプショナルプロパティ

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

TypeScript
type User = {
  name: string;
  age: number;
  email?: string;      // オプショナル
  phone?: string;      // オプショナル
};

// emailとphoneは省略可能
let user1: User = {
  name: "太郎",
  age: 25
};

let user2: User = {
  name: "花子",
  age: 30,
  email: "hanako@example.com"
};

readonlyプロパティ

変更不可能なプロパティを定義できます。

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

let config: Config = {
  apiKey: "abc123",
  endpoint: "https://api.example.com",
  timeout: 3000
};

config.timeout = 5000;  // OK
// config.apiKey = "xyz";  // エラー!readonlyは変更不可

インデックスシグネチャ

プロパティ名が動的な場合に使います。

TypeScript
// 任意の文字列キーで数値を持つオブジェクト
type Dictionary = {
  [key: string]: number;
};

let scores: Dictionary = {
  "math": 90,
  "english": 85,
  "science": 92
};

scores["history"] = 88;  // 動的に追加可能

// 実用例:ユーザー情報の辞書
type UserDatabase = {
  [userId: string]: {
    name: string;
    email: string;
  };
};

let users: UserDatabase = {
  "user001": { name: "太郎", email: "taro@example.com" },
  "user002": { name: "花子", email: "hanako@example.com" }
};

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

TypeScript
type MixedType = {
  name: string;           // 必須プロパティ
  age: number;            // 必須プロパティ
  [key: string]: any;     // その他は任意
};

let person: MixedType = {
  name: "太郎",
  age: 25,
  hobby: "reading",      // 追加のプロパティOK
  country: "Japan"       // 追加のプロパティOK
};

Type Alias vs Interface

オブジェクトの型定義には2つの方法があります。

Type Alias

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

type Admin = User & {
  role: "admin";
  permissions: string[];
};

Interface

TypeScript
interface User {
  name: string;
  age: number;
}

interface Admin extends User {
  role: "admin";
  permissions: string[];
}

主な違い

TypeScript
// 1. Type Aliasは様々な型に使える
type ID = string | number;  // Union型
type Point = [number, number];  // タプル

// 2. Interfaceは宣言のマージが可能
interface Window {
  title: string;
}
interface Window {
  width: number;
}
// 自動的にマージされる
const w: Window = { title: "App", width: 800 };

// 3. Type Aliasは型の計算が可能
type ReadonlyUser = {
  readonly [K in keyof User]: User[K];
};

どちらを使うべき?

  • オブジェクトの形を定義するなら、基本的にInterfaceを使う(拡張しやすい)
  • Union型やタプルなど、複雑な型定義にはType Aliasを使う
  • チーム内で統一されていればどちらでもOK

ネストしたオブジェクト

実務では複雑なオブジェクト構造を扱います。

TypeScript
type Company = {
  name: string;
  employees: {
    id: number;
    name: string;
    department: {
      id: number;
      name: string;
      manager: {
        id: number;
        name: string;
      };
    };
  }[];
};

// より読みやすく分割する
type Manager = {
  id: number;
  name: string;
};

type Department = {
  id: number;
  name: string;
  manager: Manager;
};

type Employee = {
  id: number;
  name: string;
  department: Department;
};

type Company2 = {
  name: string;
  employees: Employee[];
};

実践例:ECサイトの商品管理

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

TypeScript
// 商品カテゴリ(リテラル型)
type Category = "electronics" | "clothing" | "food" | "books";

// 在庫ステータス
type StockStatus = "in_stock" | "low_stock" | "out_of_stock";

// レビュー
type Review = {
  id: number;
  userId: string;
  rating: 1 | 2 | 3 | 4 | 5;  // 1〜5の評価
  comment: string;
  createdAt: Date;
};

// 商品
type Product = {
  id: number;
  name: string;
  description: string;
  price: number;
  category: Category;
  stockStatus: StockStatus;
  images: string[];  // 画像URLの配列
  tags: readonly string[];  // タグは変更不可
  specifications?: {  // 仕様(オプショナル)
    [key: string]: string | number;
  };
  reviews: Review[];
};

// 使用例
const laptop: Product = {
  id: 1,
  name: "高性能ノートPC",
  description: "最新のプロセッサを搭載",
  price: 150000,
  category: "electronics",
  stockStatus: "in_stock",
  images: [
    "https://example.com/laptop1.jpg",
    "https://example.com/laptop2.jpg"
  ],
  tags: ["人気", "新製品"],
  specifications: {
    cpu: "Intel Core i7",
    ram: 16,
    storage: 512
  },
  reviews: [
    {
      id: 1,
      userId: "user001",
      rating: 5,
      comment: "とても満足しています",
      createdAt: new Date("2024-01-15")
    }
  ]
};

// 商品を検索する関数
function findProductsByCategory(
  products: Product[],
  category: Category
): Product[] {
  return products.filter(p => p.category === category);
}

// 在庫が少ない商品を取得
function getLowStockProducts(products: Product[]): Product[] {
  return products.filter(p => p.stockStatus === "low_stock");
}

// 平均評価を計算
function calculateAverageRating(product: Product): number {
  if (product.reviews.length === 0) return 0;
  
  const sum = product.reviews.reduce((acc, review) => acc + review.rating, 0);
  return sum / product.reviews.length;
}

型の互換性

TypeScriptは構造的型付け(Structural Typing)を採用しています。

TypeScript
type Point2D = {
  x: number;
  y: number;
};

type Point3D = {
  x: number;
  y: number;
  z: number;
};

let point2D: Point2D = { x: 10, y: 20 };
let point3D: Point3D = { x: 10, y: 20, z: 30 };

// Point3DはPoint2Dと互換性がある(余分なプロパティがあっても可)
point2D = point3D;  // OK

// 逆は不可(zが足りない)
// point3D = point2D;  // エラー!

よくあるパターンとアンチパターン

✅ 良い例

TypeScript
// 明確な型定義
type User = {
  id: number;
  name: string;
  email: string;
};

// 適切な Union型の使用
type Result = { success: true; data: string } | { success: false; error: string };

// readonlyで不変性を保証
type Config = {
  readonly apiKey: string;
};

❌ 避けるべき例

TypeScript
// any型の使用(型安全性が失われる)
let data: any = "文字列";
data.toString();  // エラーチェックされない

// 過度に複雑なネスト
type BadType = {
  a: {
    b: {
      c: {
        d: {
          e: string;  // 深すぎる
        };
      };
    };
  };
};

// 型を明示しない空配列
let items = [];  // any[]になってしまう

まとめ

今回は、TypeScriptの型システムの基礎を深く学びました。

学んだこと

  • プリミティブ型の詳細とリテラル型
  • 配列、タプル、readonly配列
  • オブジェクトの型定義とオプショナルプロパティ
  • Type AliasとInterfaceの使い分け
  • インデックスシグネチャ
  • 実践的な型定義の例

次回予告 第3回では、関数の型定義とジェネリクスについて学びます。より柔軟で再利用可能なコードを書けるようになります。

TypeScript入門シリーズ

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