TypeScript

TypeScript入門|型安全なJavaScript開発を始める

TypeScript JavaScript 型システム

TypeScript入門
型安全なJavaScript開発を始める

TypeScriptの基本的な型システムを基礎から解説。型定義、インターフェース、ジェネリクス、型ガードまで、型安全な開発の基礎を学べます。

こんな人向けの記事です

  • TypeScriptを初めて学ぶ方
  • JavaScriptの型の問題を解決したい方
  • インターフェースやジェネリクスを理解したい方

Step 1TypeScriptとは

TypeScriptは、Microsoftが開発したJavaScriptに静的型付けを追加したプログラミング言語です。TypeScriptのコードはコンパイル時にJavaScriptに変換されるため、JavaScriptが動作するすべての環境で利用できます。

ポイント

TypeScriptは「JavaScriptのスーパーセット」です。すべてのJavaScriptコードはそのままTypeScriptとしても有効です。既存のJSプロジェクトに段階的に導入できるのが大きなメリットです。

JavaScriptとTypeScriptの主な違いを比較してみましょう。

特徴 JavaScript TypeScript
型システム 動的型付け 静的型付け(コンパイル時チェック)
エラー検出 実行時 コンパイル時(開発中に発見)
IDE補完 限定的 強力な自動補完・リファクタリング
実行環境 ブラウザ・Node.jsで直接実行 コンパイル後にJSとして実行
学習コスト 低い 型の知識が必要(段階的に学習可能)

JavaScriptで起きやすい型エラーをTypeScriptがどう防ぐか見てみましょう。

JavaScript(実行時エラー)
// JavaScriptでは実行するまでエラーに気づけない
function greet(name) {
  return "Hello, " + name.toUpperCase();
}

greet(42); // 実行時エラー: name.toUpperCase is not a function
TypeScript(コンパイル時エラー)
// TypeScriptではコンパイル時にエラーを検出
function greet(name: string): string {
  return "Hello, " + name.toUpperCase();
}

greet(42); // コンパイルエラー: Argument of type 'number' is not assignable to parameter of type 'string'

TypeScriptの開発環境をセットアップするには、Node.jsがインストールされている環境で以下のコマンドを実行します。

ターミナル
# TypeScriptをインストール
npm install -g typescript

# バージョン確認
tsc --version

# プロジェクト初期化(tsconfig.jsonを生成)
tsc --init

# TypeScriptファイルをコンパイル
tsc hello.ts

Step 2基本的な型

TypeScriptで最もよく使う基本型を見ていきましょう。変数宣言時に: 型名を付けることで型を指定できます(型アノテーション)。

プリミティブ型

basic-types.ts
// string: 文字列
let userName: string = "太郎";

// number: 数値(整数・小数の区別なし)
let age: number = 25;
let price: number = 1980.5;

// boolean: 真偽値
let isActive: boolean = true;

// null と undefined
let nothing: null = null;
let notDefined: undefined = undefined;

// any: 型チェックを無効化(使用は最小限に)
let flexible: any = "hello";
flexible = 42;      // エラーにならない
flexible = true;    // エラーにならない
注意

any型は型チェックを完全に無効化するため、TypeScriptの恩恵を受けられなくなります。やむを得ない場合を除き、具体的な型を指定するか、後述のunknown型を使いましょう。

配列(Array)

array-types.ts
// 配列の型指定(2つの書き方)
let numbers: number[] = [1, 2, 3, 4, 5];
let names: Array<string> = ["Alice", "Bob", "Charlie"];

// 型が異なる要素を追加するとエラー
numbers.push("six"); // コンパイルエラー: Argument of type 'string' is not assignable

// 読み取り専用配列
let readonlyArr: readonly number[] = [1, 2, 3];
readonlyArr.push(4); // コンパイルエラー: Property 'push' does not exist

タプル(Tuple)

tuple-types.ts
// タプル: 要素数と各要素の型が固定された配列
let user: [string, number] = ["太郎", 25];

// 各要素の型が保証される
let name: string = user[0]; // OK: string
let age: number = user[1];  // OK: number

// 型が合わないとエラー
let wrong: [string, number] = [25, "太郎"]; // コンパイルエラー

// ラベル付きタプル(可読性向上)
type UserTuple = [name: string, age: number, isAdmin: boolean];
let admin: UserTuple = ["管理者", 30, true];

列挙型(Enum)

enum-types.ts
// 数値enum(デフォルトは0から連番)
enum Direction {
  Up,     // 0
  Down,   // 1
  Left,   // 2
  Right,  // 3
}

let move: Direction = Direction.Up;
console.log(move); // 0

// 文字列enum(値を明示的に指定)
enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE",
  Pending = "PENDING",
}

let currentStatus: Status = Status.Active;
console.log(currentStatus); // "ACTIVE"
ポイント

文字列enumはデバッグ時に値が読みやすく、意図しない比較を防げるため、数値enumより推奨されます。ただし、後述のユニオン型で代替できるケースも多いです。

基本型の全体像をまとめます。

説明
string 文字列 "hello"
number 数値(整数・小数) 42, 3.14
boolean 真偽値 true, false
Array<T> 配列 [1, 2, 3]
[T, U] タプル ["name", 25]
enum 列挙型 Direction.Up
any 型チェック無効 なるべく使わない
unknown 安全なany 型ガードと組み合わせる
void 戻り値なし 関数の戻り値
never 到達しない型 例外を投げる関数

Step 3インターフェースと型エイリアス

オブジェクトの構造(どんなプロパティを持つか)を定義する方法として、インターフェース型エイリアスの2つがあります。

インターフェース(interface)

interface.ts
// インターフェースでオブジェクトの形を定義
interface User {
  id: number;
  name: string;
  email: string;
  age?: number;          // ?を付けるとオプショナル(省略可能)
  readonly createdAt: Date; // readonlyで変更不可
}

// インターフェースに従ったオブジェクトを作成
const user: User = {
  id: 1,
  name: "太郎",
  email: "taro@example.com",
  createdAt: new Date(),
  // age は省略可能
};

user.createdAt = new Date(); // コンパイルエラー: readonlyプロパティは変更不可

// 必須プロパティが欠けるとエラー
const badUser: User = {
  id: 2,
  name: "花子",
  // email が無い → コンパイルエラー
};

インターフェースの拡張(extends)

interface-extends.ts
// 基本インターフェース
interface Animal {
  name: string;
  age: number;
}

// 拡張(継承)
interface Dog extends Animal {
  breed: string;
  bark(): void;
}

// Dog は Animal のプロパティも持つ
const dog: Dog = {
  name: "ポチ",
  age: 3,
  breed: "柴犬",
  bark() {
    console.log("ワン!");
  },
};

// 複数のインターフェースを拡張
interface Pet extends Animal {
  owner: string;
}

interface PetDog extends Dog, Pet {
  trained: boolean;
}

型エイリアス(type)

type-alias.ts
// 型エイリアスでオブジェクトの形を定義
type Product = {
  id: number;
  name: string;
  price: number;
  category: string;
};

// プリミティブ型にも別名を付けられる(interfaceではできない)
type ID = number | string;
type Callback = (data: string) => void;

// 交差型(&)で型を合成
type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};

type TimestampedProduct = Product & Timestamped;

const product: TimestampedProduct = {
  id: 1,
  name: "ノートPC",
  price: 98000,
  category: "電子機器",
  createdAt: new Date(),
  updatedAt: new Date(),
};

インターフェースと型エイリアスの使い分けをまとめます。

機能 interface type
オブジェクト型の定義 可能 可能
拡張(extends) 可能 交差型(&)で代替
宣言マージ 可能(同名で自動統合) 不可
プリミティブ型の別名 不可 可能
ユニオン型の定義 不可 可能
推奨シーン オブジェクト・クラスの形状定義 ユニオン型・複合型・関数型
ポイント

迷ったらinterfaceを使い、ユニオン型や関数型などinterfaceで表現できないものにtypeを使うのが一般的なベストプラクティスです。

Step 4ユニオン型とリテラル型

ユニオン型は「AまたはB」のように、複数の型のいずれかを取れる値を表現します。リテラル型と組み合わせることで、取りうる値を厳密に制限できます。

ユニオン型

union-types.ts
// ユニオン型: | で複数の型を許容
let id: string | number;
id = "abc-123";  // OK
id = 42;         // OK
id = true;       // コンパイルエラー: boolean は許容されない

// 関数の引数にユニオン型を使用
function formatId(id: string | number): string {
  // 共通のメソッドしか使えない(型を絞り込むまで)
  return String(id).padStart(5, "0");
}

console.log(formatId("123"));  // "00123"
console.log(formatId(42));     // "00042"

// 配列のユニオン型
let mixed: (string | number)[] = [1, "two", 3, "four"];

リテラル型

literal-types.ts
// リテラル型: 特定の値のみ許容する型
type Direction = "north" | "south" | "east" | "west";

let heading: Direction = "north"; // OK
heading = "northeast";            // コンパイルエラー

// 数値リテラル型
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 3; // OK
roll = 7;               // コンパイルエラー

// 実用例: HTTPメソッド
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";

function fetchData(url: string, method: HttpMethod): void {
  console.log(url, method);
}

fetchData("/api/users", "GET");    // OK
fetchData("/api/users", "REMOVE"); // コンパイルエラー

判別可能なユニオン型(Discriminated Union)

discriminated-union.ts
// 共通のプロパティ(kind)で型を判別する
interface Circle {
  kind: "circle";
  radius: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

interface Triangle {
  kind: "triangle";
  base: number;
  height: number;
}

type Shape = Circle | Rectangle | Triangle;

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      // ここでは shape は Circle 型に絞り込まれる
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      // ここでは shape は Rectangle 型に絞り込まれる
      return shape.width * shape.height;
    case "triangle":
      // ここでは shape は Triangle 型に絞り込まれる
      return (shape.base * shape.height) / 2;
  }
}

console.log(calculateArea({ kind: "circle", radius: 5 }));           // 78.54
console.log(calculateArea({ kind: "rectangle", width: 4, height: 6 })); // 24
ポイント

判別可能なユニオン型はTypeScriptで頻出するパターンです。Reduxのアクション定義、APIレスポンスの処理、状態管理など、多くの場面で活用されます。

Step 5ジェネリクス

ジェネリクスは「型をパラメータとして受け取る」仕組みです。関数やクラスを特定の型に依存させず、再利用可能にしながら型安全性を保てます。

ジェネリック関数

generics-function.ts
// anyを使うと型情報が失われる(悪い例)
function firstElementBad(arr: any[]): any {
  return arr[0];
}
const result1 = firstElementBad([1, 2, 3]); // 型は any

// ジェネリクスを使うと型が保持される(良い例)
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

const num = firstElement([1, 2, 3]);        // 型は number | undefined
const str = firstElement(["a", "b", "c"]);  // 型は string | undefined

// 複数の型パラメータ
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const p = pair("hello", 42); // 型は [string, number]

ジェネリック制約(extends)

generics-constraint.ts
// T に制約を設ける: length プロパティを持つ型のみ許容
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(value: T): void {
  console.log(value.length);
}

logLength("hello");     // OK: string は length を持つ
logLength([1, 2, 3]);   // OK: 配列は length を持つ
logLength(42);          // コンパイルエラー: number は length を持たない

// keyof を使った制約
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "太郎", age: 25, email: "taro@example.com" };
const name = getProperty(user, "name");  // 型は string
const age = getProperty(user, "age");    // 型は number
getProperty(user, "phone");              // コンパイルエラー: "phone" は User のキーではない

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

generics-interface.ts
// APIレスポンスの共通型
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: Date;
}

// 具体的な型を指定して使用
interface User {
  id: number;
  name: string;
}

interface Product {
  id: number;
  name: string;
  price: number;
}

// 同じ ApiResponse 型を異なるデータ型で再利用
const userResponse: ApiResponse<User> = {
  data: { id: 1, name: "太郎" },
  status: 200,
  message: "Success",
  timestamp: new Date(),
};

const productResponse: ApiResponse<Product[]> = {
  data: [
    { id: 1, name: "ノートPC", price: 98000 },
    { id: 2, name: "マウス", price: 3500 },
  ],
  status: 200,
  message: "Success",
  timestamp: new Date(),
};
ポイント

ジェネリクスは「中身は何でもいいが、一貫性は保ちたい」ときに使います。ApiResponse<T>のように、レスポンスの構造は同じだがデータの型だけ変わる場面で威力を発揮します。

Step 6型ガードとナローイング

ユニオン型の変数に対して、条件分岐で型を絞り込む(ナローイングする)技法を型ガードと呼びます。TypeScriptコンパイラは条件分岐を解析して、各ブロック内で型を自動的に絞り込みます。

typeof による型ガード

typeof-guard.ts
function processValue(value: string | number | boolean): string {
  // typeof で型を絞り込む
  if (typeof value === "string") {
    // ここでは value は string 型
    return value.toUpperCase();
  } else if (typeof value === "number") {
    // ここでは value は number 型
    return value.toFixed(2);
  } else {
    // ここでは value は boolean 型
    return value ? "YES" : "NO";
  }
}

console.log(processValue("hello")); // "HELLO"
console.log(processValue(3.14));    // "3.14"
console.log(processValue(true));    // "YES"

instanceof による型ガード

instanceof-guard.ts
class Dog {
  bark(): string {
    return "ワンワン!";
  }
}

class Cat {
  meow(): string {
    return "ニャー!";
  }
}

function makeSound(animal: Dog | Cat): string {
  if (animal instanceof Dog) {
    // ここでは animal は Dog 型
    return animal.bark();
  } else {
    // ここでは animal は Cat 型
    return animal.meow();
  }
}

console.log(makeSound(new Dog())); // "ワンワン!"
console.log(makeSound(new Cat())); // "ニャー!"

in 演算子による型ガード

in-guard.ts
interface Fish {
  swim: () => void;
}

interface Bird {
  fly: () => void;
}

function move(animal: Fish | Bird): void {
  if ("swim" in animal) {
    // ここでは animal は Fish 型
    animal.swim();
  } else {
    // ここでは animal は Bird 型
    animal.fly();
  }
}

ユーザー定義型ガード

custom-guard.ts
interface Admin {
  role: "admin";
  permissions: string[];
}

interface Guest {
  role: "guest";
  visitCount: number;
}

type User = Admin | Guest;

// 戻り値の型「user is Admin」が型ガードの宣言
function isAdmin(user: User): user is Admin {
  return user.role === "admin";
}

function showDashboard(user: User): void {
  if (isAdmin(user)) {
    // ここでは user は Admin 型
    console.log("権限:", user.permissions.join(", "));
  } else {
    // ここでは user は Guest 型
    console.log("訪問回数:", user.visitCount);
  }
}

showDashboard({ role: "admin", permissions: ["read", "write", "delete"] });
// 出力: 権限: read, write, delete

showDashboard({ role: "guest", visitCount: 5 });
// 出力: 訪問回数: 5
注意

ユーザー定義型ガードの判定ロジックが間違っていると、コンパイラは信用してしまうため実行時エラーの原因になります。型ガード関数のテストは必ず書きましょう。

Exhaustiveness Check(網羅性チェック)

exhaustiveness.ts
type Status = "active" | "inactive" | "pending";

function getStatusLabel(status: Status): string {
  switch (status) {
    case "active":
      return "有効";
    case "inactive":
      return "無効";
    case "pending":
      return "保留中";
    default:
      // never型に代入: すべてのケースを処理していることを保証
      const _exhaustive: never = status;
      return _exhaustive;
  }
}

// 将来 Status に "archived" を追加した場合、
// switch文で処理しないと default でコンパイルエラーになる
ポイント

網羅性チェックはnever型を使ったテクニックです。ユニオン型に新しいメンバーを追加したとき、処理を書き忘れた箇所をコンパイラが教えてくれるため、バグの防止に非常に効果的です。

TypeScript入門チェックリスト

  • TypeScriptはJavaScriptのスーパーセットで、静的型付けを追加する
  • 基本型: string, number, boolean, Array, Tuple, Enum
  • interfaceはオブジェクトの形を定義し、extendsで拡張できる
  • typeはユニオン型・交差型・関数型など柔軟な型定義に使う
  • ユニオン型(A | B)で複数の型を許容、リテラル型で値を制限
  • ジェネリクスで型をパラメータ化し、再利用性と型安全性を両立
  • 型ガード(typeof, instanceof, in, ユーザー定義)で型を絞り込む
  • never型で網羅性チェックを行い、処理漏れを防ぐ