このチュートリアルで学ぶこと
✓ 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/**/*"]
}
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...
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. 型推論を活用
- 明示的な型注釈は必要な場所のみ
- 関数の戻り値は注釈を付けると良い
2. anyを避ける
- unknownを使って型チェックを行う
- 型が分からない場合はジェネリクスを検討
3. strictモードを有効化
- tsconfig.jsonで"strict": true
- より安全なコードが書ける
4. 適切な型定義を選択
- オブジェクト → interface
- ユニオン、タプル → type
5. ユーティリティ型を活用
- 既存の型から新しい型を導出
- 重複を避けて保守性を向上
まとめ
TypeScriptの型システムを活用することで、コンパイル時にエラーを検出し、より安全なコードを書けます。基本の型から始めて、ジェネリクスやユーティリティ型を使いこなせるようになりましょう。
← 一覧に戻る