TypeScript入門 - 型安全なJavaScript開発

入門 | 50分 で読める | 2024.12.16

公式ドキュメント

このチュートリアルで学ぶこと

  • TypeScriptの基本的な型
  • 型推論の仕組み
  • インターフェースと型エイリアス
  • ユニオン型とリテラル型
  • ジェネリクスの基本
  • 実践的な型定義パターン

前提条件

  • JavaScriptの基本知識
  • Node.jsがインストールされていること

プロジェクトのセットアップ

# プロジェクトディレクトリ作成
mkdir typescript-tutorial
cd typescript-tutorial

# package.json作成
npm init -y

# TypeScriptインストール
npm install -D typescript ts-node @types/node

# tsconfig.json作成
npx tsc --init

tsconfig.jsonの設定

{
 "compilerOptions": {
 "target": "ES2020",
 "module": "commonjs",
 "strict": true,
 "esModuleInterop": true,
 "skipLibCheck": true,
 "forceConsistentCasingInFileNames": true,
 "outDir": "./dist"
 },
 "include": ["src/**/*"]
}

ポイント: TypeScriptは型推論が強力なので、すべてに型注釈を付ける必要はありません。変数の初期値から自動的に型が推論されます。明示的な型注釈は関数の引数と戻り値に優先的に付けましょう。

Step 1: 基本的な型

プリミティブ型

// src/01-primitives.ts

// 文字列
let name: string = "太郎";
let greeting: string = `こんにちは、${name}さん`;

// 数値
let age: number = 25;
let price: number = 1980.5;
let hex: number = 0xff;

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

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

// any(型チェックを無効化 - できるだけ避ける)
let anything: any = "string";
anything = 123;
anything = true;

// unknown(anyより安全)
let unknownValue: unknown = "hello";
// unknownValue.toUpperCase(); // エラー
if (typeof unknownValue === "string") {
 unknownValue.toUpperCase(); // OK
}

配列とタプル

// src/02-arrays.ts

// 配列
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];

// ジェネリック記法
let values: Array<number> = [10, 20, 30];

// タプル(固定長・固定型の配列)
let tuple: [string, number] = ["年齢", 25];
let rgb: [number, number, number] = [255, 128, 0];

// タプルのラベル付き
let user: [name: string, age: number] = ["太郎", 30];

// 読み取り専用配列
let readonlyNumbers: readonly number[] = [1, 2, 3];
// readonlyNumbers.push(4); // エラー

オブジェクト型

// src/03-objects.ts

// オブジェクト型の定義
let person: { name: string; age: number } = {
 name: "花子",
 age: 28
};

// オプショナルプロパティ
let config: { host: string; port?: number } = {
 host: "localhost"
 // portは省略可能
};

// 読み取り専用プロパティ
let point: { readonly x: number; readonly y: number } = {
 x: 10,
 y: 20
};
// point.x = 30; // エラー

// インデックスシグネチャ
let dictionary: { [key: string]: string } = {
 hello: "こんにちは",
 goodbye: "さようなら"
};
dictionary["thanks"] = "ありがとう"; // OK

Step 2: 関数の型

基本的な関数型

// src/04-functions.ts

// パラメータと戻り値の型
function add(a: number, b: number): number {
 return a + b;
}

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

// オプショナルパラメータ
function greet(name: string, greeting?: string): string {
 return `${greeting || "Hello"}, ${name}!`;
}

// デフォルトパラメータ
function createUser(
 name: string,
 age: number = 20,
 role: string = "user"
): { name: string; age: number; role: string } {
 return { name, age, role };
}

// 残余パラメータ
function sum(...numbers: number[]): number {
 return numbers.reduce((acc, curr) => acc + curr, 0);
}

console.log(sum(1, 2, 3, 4, 5)); // 15

関数型の定義

// 関数型の変数
let calculator: (a: number, b: number) => number;
calculator = (x, y) => x + y;

// 型エイリアスで関数型を定義
type MathOperation = (a: number, b: number) => number;

const subtract: MathOperation = (a, b) => a - b;
const divide: MathOperation = (a, b) => a / b;

// コールバック関数
function processArray(
 arr: number[],
 callback: (item: number) => number
): number[] {
 return arr.map(callback);
}

const doubled = processArray([1, 2, 3], (x) => x * 2);
console.log(doubled); // [2, 4, 6]

void と never

// void - 戻り値がない
function logMessage(message: string): void {
 console.log(message);
}

// never - 決して戻らない
function throwError(message: string): never {
 throw new Error(message);
}

function infiniteLoop(): never {
 while (true) {}
}

実践メモ: インターフェースはオブジェクトの構造定義に、型エイリアスはユニオン型や複雑な型の定義に使い分けるのが一般的です。迷ったらインターフェースを使いましょう。

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

型エイリアス(type)

// src/05-type-alias.ts

// 基本的な型エイリアス
type ID = string | number;
type Point = { x: number; y: number };

// ユニオン型
type Status = "pending" | "approved" | "rejected";
type Result = string | null;

let userId: ID = "user_123";
let orderId: ID = 456;
let currentStatus: Status = "pending";

// 交差型
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;

const person: Person = {
 name: "太郎",
 age: 30
};

インターフェース

// src/06-interface.ts

// 基本的なインターフェース
interface User {
 id: number;
 name: string;
 email: string;
 age?: number; // オプショナル
 readonly createdAt: Date; // 読み取り専用
}

const user: User = {
 id: 1,
 name: "Alice",
 email: "alice@example.com",
 createdAt: new Date()
};

// インターフェースの継承
interface Employee extends User {
 department: string;
 salary: number;
}

const employee: Employee = {
 id: 2,
 name: "Bob",
 email: "bob@example.com",
 createdAt: new Date(),
 department: "Engineering",
 salary: 500000
};

// メソッドを持つインターフェース
interface Calculator {
 add(a: number, b: number): number;
 subtract(a: number, b: number): number;
}

const calc: Calculator = {
 add: (a, b) => a + b,
 subtract: (a, b) => a - b
};

typeとinterfaceの違い

// type: ユニオン、プリミティブ、タプルに使用可能
type StringOrNumber = string | number;
type Tuple = [string, number];

// interface: 拡張が可能(同名で宣言すると自動マージ)
interface Window {
 title: string;
}
interface Window {
 size: { width: number; height: number };
}
// → Windowは両方のプロパティを持つ

// 一般的な使い分け
// - オブジェクトの形状定義 → interface
// - ユニオン型、プリミティブ → type
// - 関数型 → type

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

ユニオン型

// src/07-union.ts

// 基本的なユニオン型
type StringOrNumber = string | number;

function printId(id: StringOrNumber) {
 if (typeof id === "string") {
 console.log(id.toUpperCase());
 } else {
 console.log(id.toFixed(2));
 }
}

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

リテラル型

// 文字列リテラル型
type Direction = "north" | "south" | "east" | "west";

function move(direction: Direction) {
 console.log(`Moving ${direction}`);
}

move("north"); // OK
// move("up"); // エラー

// 数値リテラル型
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;

function rollDice(): DiceValue {
 return Math.ceil(Math.random() * 6) as DiceValue;
}

// ブールリテラル型
type True = true;

判別可能なユニオン型

// src/08-discriminated-union.ts

interface Circle {
 kind: "circle";
 radius: number;
}

interface Square {
 kind: "square";
 sideLength: number;
}

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

type Shape = Circle | Square | Rectangle;

function calculateArea(shape: Shape): number {
 switch (shape.kind) {
 case "circle":
 return Math.PI * shape.radius ** 2;
 case "square":
 return shape.sideLength ** 2;
 case "rectangle":
 return shape.width * shape.height;
 }
}

const circle: Circle = { kind: "circle", radius: 5 };
console.log(calculateArea(circle)); // 78.54...

注意: any型の使用は型安全性を完全に無効化します。どうしても型がわからない場合はunknownを使い、型ガードで絞り込みましょう。

Step 5: ジェネリクス

基本的なジェネリクス

// src/09-generics.ts

// ジェネリック関数
function identity<T>(value: T): T {
 return value;
}

const str = identity<string>("hello");
const num = identity(42); // 型推論でnumber

// 配列を扱うジェネリック関数
function first<T>(arr: T[]): T | undefined {
 return arr[0];
}

const firstNumber = first([1, 2, 3]); // number | undefined
const firstString = first(["a", "b", "c"]); // string | undefined

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

// APIレスポンスの型
interface ApiResponse<T> {
 data: T;
 status: number;
 message: string;
}

interface User {
 id: number;
 name: string;
}

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

const userResponse: ApiResponse<User> = {
 data: { id: 1, name: "Alice" },
 status: 200,
 message: "Success"
};

const productResponse: ApiResponse<Product[]> = {
 data: [
 { id: 1, title: "Item 1", price: 1000 },
 { id: 2, title: "Item 2", price: 2000 }
 ],
 status: 200,
 message: "Success"
};

制約付きジェネリクス

// extends で型制約を追加
interface Lengthwise {
 length: number;
}

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

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

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

const person = { name: "太郎", age: 30 };
const name = getProperty(person, "name"); // string
const age = getProperty(person, "age"); // number
// getProperty(person, "email"); // エラー

Step 6: ユーティリティ型

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

// src/10-utility-types.ts

interface User {
 id: number;
 name: string;
 email: string;
 age: number;
}

// Partial - すべてオプショナルに
type PartialUser = Partial<User>;
const updateData: PartialUser = { name: "新しい名前" };

// Required - すべて必須に
type RequiredUser = Required<User>;

// Pick - 特定のプロパティのみ選択
type UserBasic = Pick<User, "id" | "name">;
const basic: UserBasic = { id: 1, name: "Alice" };

// Omit - 特定のプロパティを除外
type UserWithoutEmail = Omit<User, "email">;

// Readonly - すべて読み取り専用に
type ReadonlyUser = Readonly<User>;

// Record - キーと値の型を指定したオブジェクト
type UserRoles = Record<string, "admin" | "user" | "guest">;
const roles: UserRoles = {
 alice: "admin",
 bob: "user"
};

実践的な使用例

// フォームの状態管理
interface FormData {
 username: string;
 email: string;
 password: string;
}

// フォームのエラー状態
type FormErrors = Partial<Record<keyof FormData, string>>;

const errors: FormErrors = {
 email: "有効なメールアドレスを入力してください"
};

// APIリクエスト/レスポンス
type CreateUserRequest = Omit<User, "id">;
type UpdateUserRequest = Partial<Omit<User, "id">>;
type UserResponse = Readonly<User>;

function createUser(data: CreateUserRequest): UserResponse {
 return { id: Date.now(), ...data } as UserResponse;
}

実践課題: 型安全なTodoアプリ

// src/todo-app.ts

// 型定義
type TodoStatus = "pending" | "in_progress" | "completed";
type Priority = "low" | "medium" | "high";

interface Todo {
 id: number;
 title: string;
 description?: string;
 status: TodoStatus;
 priority: Priority;
 createdAt: Date;
 completedAt?: Date;
}

type CreateTodoInput = Omit<Todo, "id" | "createdAt" | "completedAt">;
type UpdateTodoInput = Partial<Omit<Todo, "id" | "createdAt">>;

// Todoリストクラス
class TodoList {
 private todos: Todo[] = [];
 private nextId = 1;

 add(input: CreateTodoInput): Todo {
 const todo: Todo = {
 ...input,
 id: this.nextId++,
 createdAt: new Date()
 };
 this.todos.push(todo);
 return todo;
 }

 update(id: number, input: UpdateTodoInput): Todo | null {
 const index = this.todos.findIndex(t => t.id === id);
 if (index === -1) return null;

 const updated = { ...this.todos[index], ...input };
 if (input.status === "completed" && !updated.completedAt) {
 updated.completedAt = new Date();
 }
 this.todos[index] = updated;
 return updated;
 }

 delete(id: number): boolean {
 const index = this.todos.findIndex(t => t.id === id);
 if (index === -1) return false;
 this.todos.splice(index, 1);
 return true;
 }

 getAll(): Readonly<Todo[]> {
 return this.todos;
 }

 getByStatus(status: TodoStatus): Todo[] {
 return this.todos.filter(t => t.status === status);
 }

 getByPriority(priority: Priority): Todo[] {
 return this.todos.filter(t => t.priority === priority);
 }
}

// 使用例
const todoList = new TodoList();

todoList.add({
 title: "TypeScriptを学ぶ",
 description: "基本的な型システムを理解する",
 status: "in_progress",
 priority: "high"
});

todoList.add({
 title: "Reactを学ぶ",
 status: "pending",
 priority: "medium"
});

console.log(todoList.getAll());
console.log(todoList.getByStatus("pending"));

ベストプラクティス

  1. 型推論を活用
  • 明示的な型注釈は必要な場所のみ
  • 関数の戻り値は注釈を付けると良い
  1. anyを避ける
  • unknownを使って型チェックを行う
  • 型が分からない場合はジェネリクスを検討
  1. strictモードを有効化
  • tsconfig.jsonで”strict”: true
  • より安全なコードが書ける
  1. 適切な型定義を選択
  • オブジェクト → interface
  • ユニオン、タプル → type
  1. ユーティリティ型を活用
  • 既存の型から新しい型を導出
  • 重複を避けて保守性を向上

現場でよくあるTypeScriptの課題と解決策

1. 外部APIのレスポンス型定義

// 実際のプロジェクトでは、zodやio-tsで実行時検証も行う
import { z } from 'zod';

const UserSchema = z.object({
 id: z.number(),
 name: z.string(),
 email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

async function fetchUser(id: number): Promise<User> {
 const response = await fetch(`/api/users/${id}`);
 const data = await response.json();
 return UserSchema.parse(data); // 実行時に型チェック
}

2. 既存JavaScriptプロジェクトの段階的移行

// 1. まずtsconfig.jsonで緩いルールから開始
{
 "compilerOptions": {
 "strict": false,
 "noImplicitAny": false,
 "allowJs": true
 }
}

// 2. 徐々にstrictを有効化していく
// 3. @ts-checkでJSファイルにも型チェックを適用

3. 型定義ファイルがないライブラリ

// @types/xxx がない場合は自分で定義
// src/types/legacy-lib.d.ts
declare module 'legacy-lib' {
 export function doSomething(input: string): number;
 export interface Options {
 timeout?: number;
 }
}

関連記事

TypeScriptをさらに活用するために:

まとめ

TypeScriptの型システムを活用することで、コンパイル時にエラーを検出し、より安全なコードを書けます。基本の型から始めて、ジェネリクスやユーティリティ型を使いこなせるようになりましょう。

実際のプロジェクトでは、外部APIの型定義やレガシーコードとの共存など、課題も多いですが、段階的に導入することで、開発効率と品質を大幅に向上させることができます。

参考リソース

← 一覧に戻る
PR
PR
PR
PR