TypeScript実践入門:型安全性を活かしたモダンな開発
TypeScriptは、JavaScriptに静的型付けを追加したスーパーセットです。この記事では、TypeScriptの基本から始めて、実践的な開発で活用できる高度な機能までを学びます。型安全性を活かして、保守性の高いコードを書けるようになることを目指します。
JavaScript開発者でもスムーズに移行できるよう、段階的に解説していきます。
1. TypeScriptとは
TypeScriptは、Microsoftが開発したオープンソースのプログラミング言語で、JavaScriptに以下の機能を追加します。
- 静的型付け: コンパイル時に型チェック
- 型推論: 自動的な型の推測
- 最新のJavaScript機能: ES6+の機能を先取り
- 優れたツールサポート: IDEでの自動補完やリファクタリング
1.1 環境の準備
# TypeScriptのインストール
npm install -g typescript
# ローカルインストール
npm install --save-dev typescript
# バージョンの確認
tsc --version
# tsconfig.jsonの生成
tsc --init
1.2 基本的な使用
// hello.ts
function greet(name: string): string {
return `Hello, ${name}!`;
}
const message = greet("TypeScript");
console.log(message);
// コンパイル
// tsc hello.ts
// node hello.js
2. 基本的な型
2.1 プリミティブ型
// 数値
let age: number = 30;
let price: number = 99.99;
// 文字列
let name: string = "Alice";
let message: string = `Hello, ${name}`;
// 真偽値
let isActive: boolean = true;
let isComplete: boolean = false;
// nullとundefined
let nullValue: null = null;
let undefinedValue: undefined = undefined;
// any(型チェックを回避 - 避けるべき)
let anything: any = "can be anything";
anything = 42;
anything = true;
// unknown(anyより安全)
let userInput: unknown;
userInput = 5;
userInput = "hello";
// unknownを使う場合は型チェックが必要
if (typeof userInput === "string") {
console.log(userInput.toUpperCase());
}
// void(何も返さない関数)
function logMessage(message: string): void {
console.log(message);
}
// never(決して到達しない)
function throwError(message: string): never {
throw new Error(message);
}
2.2 配列とタプル
// 配列
let numbers: number[] = [1, 2, 3, 4, 5];
let names: Array<string> = ["Alice", "Bob", "Charlie"];
// 読み取り専用配列
let readonlyNumbers: ReadonlyArray<number> = [1, 2, 3];
// readonlyNumbers.push(4); // エラー
// タプル(固定長の配列)
let tuple: [string, number] = ["Alice", 30];
console.log(tuple[0]); // "Alice"
console.log(tuple[1]); // 30
// ラベル付きタプル(TypeScript 4.0+)
let labeledTuple: [name: string, age: number] = ["Alice", 30];
// オプショナルなタプル要素
let optionalTuple: [string, number?] = ["Alice"];
2.3 オブジェクト型
// オブジェクト型の定義
let user: {
name: string;
age: number;
email: string;
} = {
name: "Alice",
age: 30,
email: "alice@example.com"
};
// オプショナルプロパティ
let optionalUser: {
name: string;
age?: number; // オプショナル
} = {
name: "Bob"
};
// 読み取り専用プロパティ
let readonlyUser: {
readonly id: number;
name: string;
} = {
id: 1,
name: "Charlie"
};
// readonlyUser.id = 2; // エラー
// インデックスシグネチャ
let flexibleObject: {
[key: string]: number;
} = {
apples: 5,
bananas: 3,
oranges: 8
};
3. 型エイリアスとインターフェース
3.1 型エイリアス
// 型エイリアスの定義
type User = {
id: number;
name: string;
email: string;
age?: number;
};
// 使用
const user: User = {
id: 1,
name: "Alice",
email: "alice@example.com"
};
// ユニオン型
type Status = "pending" | "approved" | "rejected";
type ID = number | string;
let currentStatus: Status = "pending";
let userId: ID = 123;
userId = "user-123";
// 交差型(Intersection Types)
type Employee = {
name: string;
employeeId: number;
};
type Manager = {
department: string;
teamSize: number;
};
type ManagerEmployee = Employee & Manager;
const manager: ManagerEmployee = {
name: "Alice",
employeeId: 1,
department: "Engineering",
teamSize: 5
};
// 型エイリアスの再利用
type Point = {
x: number;
y: number;
};
type Circle = {
center: Point;
radius: number;
};
3.2 インターフェース
// インターフェースの定義
interface User {
id: number;
name: string;
email: string;
age?: number; // オプショナル
readonly createdAt: Date; // 読み取り専用
}
// インターフェースの拡張
interface Admin extends User {
permissions: string[];
role: "admin" | "super-admin";
}
const admin: Admin = {
id: 1,
name: "Alice",
email: "alice@example.com",
createdAt: new Date(),
permissions: ["read", "write", "delete"],
role: "admin"
};
// 複数のインターフェースの拡張
interface Person {
name: string;
age: number;
}
interface Employee {
employeeId: number;
department: string;
}
interface Manager extends Person, Employee {
teamSize: number;
}
// 関数型のインターフェース
interface Calculator {
(a: number, b: number): number;
}
const add: Calculator = (a, b) => a + b;
const multiply: Calculator = (a, b) => a * b;
// インデックスシグネチャ
interface StringDictionary {
[key: string]: string;
}
const dictionary: StringDictionary = {
hello: "こんにちは",
goodbye: "さようなら"
};
3.3 型エイリアスとインターフェースの違い
// インターフェースはマージできる
interface Window {
title: string;
}
interface Window {
width: number;
}
// 結果: { title: string; width: number; }
// 型エイリアスはマージできない
// type Window = { title: string; }
// type Window = { width: number; } // エラー
// 型エイリアスはユニオン型や交差型を直接表現できる
type Status = "active" | "inactive";
type ID = number | string;
// インターフェースは拡張に適している
// 型エイリアスはユニオン型や複雑な型に適している
4. 関数
4.1 関数の型定義
// 関数宣言
function add(a: number, b: number): number {
return a + b;
}
// 関数式
const subtract = function(a: number, b: number): number {
return a - b;
};
// アロー関数
const multiply = (a: number, b: number): number => {
return a * b;
};
// 簡潔なアロー関数
const divide = (a: number, b: number): number => a / b;
// オプショナルパラメータ
function greet(name: string, title?: string): string {
if (title) {
return `Hello, ${title} ${name}!`;
}
return `Hello, ${name}!`;
}
// デフォルトパラメータ
function createUser(
name: string,
age: number = 18,
email: string = "unknown@example.com"
): User {
return { name, age, email };
}
// レストパラメータ
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
// オーバーロード
function process(value: string): string;
function process(value: number): number;
function process(value: string | number): string | number {
if (typeof value === "string") {
return value.toUpperCase();
}
return value * 2;
}
4.2 関数型と高階関数
// 関数型の定義
type BinaryOperation = (a: number, b: number) => number;
const add: BinaryOperation = (a, b) => a + b;
const multiply: BinaryOperation = (a, b) => a * b;
// 高階関数
function applyOperation(
a: number,
b: number,
operation: BinaryOperation
): number {
return operation(a, b);
}
console.log(applyOperation(5, 3, add)); // 8
console.log(applyOperation(5, 3, multiply)); // 15
// ジェネリック関数
function identity<T>(arg: T): T {
return arg;
}
const stringValue = identity<string>("hello");
const numberValue = identity<number>(42);
const inferredValue = identity("hello"); // 型推論
// 複数の型パラメータ
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const stringNumberPair = pair("hello", 42);
5. クラス
5.1 基本的なクラス
class User {
// プロパティ
private id: number;
public name: string;
protected email: string;
readonly createdAt: Date;
// コンストラクタ
constructor(id: number, name: string, email: string) {
this.id = id;
this.name = name;
this.email = email;
this.createdAt = new Date();
}
// メソッド
public getInfo(): string {
return `${this.name} (${this.email})`;
}
// プライベートメソッド
private validateEmail(email: string): boolean {
return email.includes("@");
}
}
// 使用
const user = new User(1, "Alice", "alice@example.com");
console.log(user.getInfo());
5.2 アクセス修飾子とプロパティ
class BankAccount {
// パブリックプロパティ(デフォルト)
public accountNumber: string;
// プライベートプロパティ
private balance: number;
// プロテクトプロパティ
protected owner: string;
// 読み取り専用プロパティ
readonly createdAt: Date;
constructor(accountNumber: string, owner: string, initialBalance: number = 0) {
this.accountNumber = accountNumber;
this.owner = owner;
this.balance = initialBalance;
this.createdAt = new Date();
}
// ゲッター
public getBalance(): number {
return this.balance;
}
// メソッド
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
}
}
public withdraw(amount: number): boolean {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
return true;
}
return false;
}
}
// パラメータプロパティ(簡潔な書き方)
class Point {
constructor(
public x: number,
public y: number,
private readonly id: string
) {}
}
const point = new Point(10, 20, "point-1");
console.log(point.x, point.y);
5.3 継承と抽象クラス
// 基底クラス
class Animal {
constructor(public name: string, protected age: number) {}
public makeSound(): void {
console.log("Some generic animal sound");
}
public getInfo(): string {
return `${this.name} is ${this.age} years old`;
}
}
// 継承
class Dog extends Animal {
constructor(name: string, age: number, public breed: string) {
super(name, age);
}
// メソッドのオーバーライド
public makeSound(): void {
console.log("Woof! Woof!");
}
public fetch(): void {
console.log(`${this.name} is fetching!`);
}
}
const dog = new Dog("Buddy", 3, "Golden Retriever");
dog.makeSound(); // "Woof! Woof!"
dog.fetch();
// 抽象クラス
abstract class Shape {
constructor(public color: string) {}
// 抽象メソッド(サブクラスで実装が必要)
abstract getArea(): number;
abstract getPerimeter(): number;
// 具象メソッド
public getColor(): string {
return this.color;
}
}
class Circle extends Shape {
constructor(color: string, private radius: number) {
super(color);
}
getArea(): number {
return Math.PI * this.radius * this.radius;
}
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(color: string, private width: number, private height: number) {
super(color);
}
getArea(): number {
return this.width * this.height;
}
getPerimeter(): number {
return 2 * (this.width + this.height);
}
}
6. ジェネリクス
6.1 基本的なジェネリクス
// ジェネリック関数
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const numbers = [1, 2, 3];
const firstNumber = getFirstElement(numbers); // number | undefined
const strings = ["a", "b", "c"];
const firstString = getFirstElement(strings); // string | undefined
// ジェネリッククラス
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2
const stringStack = new Stack<string>();
stringStack.push("hello");
6.2 制約付きジェネリクス
// 型制約
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello"); // OK
logLength([1, 2, 3]); // OK
// logLength(42); // エラー
// キー制約
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
const age = getProperty(user, "age"); // number
// const invalid = getProperty(user, "email"); // エラー
// デフォルト型パラメータ
interface Container<T = string> {
value: T;
}
const stringContainer: Container = { value: "hello" };
const numberContainer: Container<number> = { value: 42 };
6.3 ユーティリティ型
interface User {
id: number;
name: string;
email: string;
age: number;
active: boolean;
}
// Partial(すべてのプロパティをオプショナルに)
type PartialUser = Partial<User>;
// { id?: number; name?: string; ... }
// Required(すべてのプロパティを必須に)
type RequiredUser = Required<PartialUser>;
// Readonly(すべてのプロパティを読み取り専用に)
type ReadonlyUser = Readonly<User>;
// Pick(特定のプロパティを選択)
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }
// Omit(特定のプロパティを除外)
type UserWithoutId = Omit<User, "id">;
// { name: string; email: string; age: number; active: boolean }
// Record(キーと値の型を指定)
type UserRoles = Record<string, string[]>;
// { [key: string]: string[] }
// Exclude(型から特定の型を除外)
type NonNullableUser = Exclude<User | null | undefined, null | undefined>;
// Extract(型から特定の型を抽出)
type StringOrNumber = Extract<string | number | boolean, string | number>;
// NonNullable(nullとundefinedを除外)
type CleanUser = NonNullable<User | null | undefined>;
7. 型ガードと型の絞り込み
7.1 typeofとinstanceof
// typeof型ガード
function processValue(value: string | number) {
if (typeof value === "string") {
// このブロックではvalueはstring型
console.log(value.toUpperCase());
} else {
// このブロックではvalueはnumber型
console.log(value.toFixed(2));
}
}
// instanceof型ガード
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
7.2 カスタム型ガード
// 型ガード関数
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // Fish型として扱える
} else {
pet.fly(); // Bird型として扱える
}
}
// in演算子
function move2(pet: Fish | Bird) {
if ("swim" in pet) {
pet.swim();
} else {
pet.fly();
}
}
// リテラル型の型ガード
type Status = "pending" | "approved" | "rejected";
function isApproved(status: Status): status is "approved" {
return status === "approved";
}
function handleStatus(status: Status) {
if (isApproved(status)) {
// statusは"approved"型
console.log("Request approved");
}
}
8. モジュールと名前空間
8.1 モジュール
// user.ts
export interface User {
id: number;
name: string;
email: string;
}
export class UserService {
private users: User[] = [];
addUser(user: User): void {
this.users.push(user);
}
getUser(id: number): User | undefined {
return this.users.find(u => u.id === id);
}
}
// デフォルトエクスポート
export default class UserRepository {
// ...
}
// main.ts
import { User, UserService } from "./user";
import UserRepository from "./user";
const service = new UserService();
const user: User = { id: 1, name: "Alice", email: "alice@example.com" };
service.addUser(user);
8.2 名前空間
// utils.ts
export namespace MathUtils {
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export const PI = 3.14159;
}
// 使用
import { MathUtils } from "./utils";
console.log(MathUtils.add(1, 2));
console.log(MathUtils.PI);
9. 実践的な例:APIクライアント
9.1 型安全なAPIクライアント
// types.ts
export interface ApiResponse<T> {
data: T;
status: number;
message?: string;
}
export interface User {
id: number;
name: string;
email: string;
}
export interface Post {
id: number;
title: string;
content: string;
authorId: number;
}
// api-client.ts
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async get<T>(endpoint: string): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseUrl}${endpoint}`);
const data = await response.json();
return {
data: data as T,
status: response.status
};
}
async post<T, U>(endpoint: string, body: U): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
const data = await response.json();
return {
data: data as T,
status: response.status
};
}
}
// 使用
const apiClient = new ApiClient("https://api.example.com");
// 型安全なAPI呼び出し
const userResponse = await apiClient.get<User>("/users/1");
console.log(userResponse.data.name); // 型チェックが効く
const postResponse = await apiClient.post<Post, Omit<Post, "id">>(
"/posts",
{
title: "Hello",
content: "World",
authorId: 1
}
);
10. 実践的な例:状態管理
10.1 型安全な状態管理
// store.ts
type Listener<T> = (state: T) => void;
class Store<T> {
private state: T;
private listeners: Listener<T>[] = [];
constructor(initialState: T) {
this.state = initialState;
}
getState(): T {
return this.state;
}
setState(newState: T): void {
this.state = newState;
this.notifyListeners();
}
updateState(updater: (state: T) => T): void {
this.state = updater(this.state);
this.notifyListeners();
}
subscribe(listener: Listener<T>): () => void {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
private notifyListeners(): void {
this.listeners.forEach(listener => listener(this.state));
}
}
// 使用
interface AppState {
user: User | null;
posts: Post[];
loading: boolean;
}
const initialState: AppState = {
user: null,
posts: [],
loading: false
};
const store = new Store<AppState>(initialState);
// 型安全な状態更新
store.updateState(state => ({
...state,
loading: true
}));
store.setState({
user: { id: 1, name: "Alice", email: "alice@example.com" },
posts: [],
loading: false
});
11. tsconfig.jsonの設定
11.1 推奨設定
{
"compilerOptions": {
// ターゲット
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
// モジュール解決
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
// 型チェック
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
// 追加チェック
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// 出力
"outDir": "./dist",
"rootDir": "./src",
"removeComments": true,
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// 実験的機能
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
12. まとめと次のステップ
この記事を通じて、TypeScriptの基礎から実践的な開発まで学びました。
学んだこと
- 基本型: プリミティブ型、配列、オブジェクト、タプル
- 型エイリアスとインターフェース: 型定義の方法
- 関数: 型定義、オーバーロード、ジェネリック関数
- クラス: アクセス修飾子、継承、抽象クラス
- ジェネリクス: 型パラメータ、制約、ユーティリティ型
- 型ガード: 型の絞り込みとカスタム型ガード
- モジュール: エクスポートとインポート
- 実践例: APIクライアント、状態管理
TypeScriptのメリット
- 型安全性: コンパイル時にエラーを検出
- 開発体験: IDEでの優れたサポート
- リファクタリング: 安全なコード変更
- ドキュメント: 型が自己文書化
- スケーラビリティ: 大規模プロジェクトでの保守性
次のステップ
- 高度な型: 条件型、マップ型、テンプレートリテラル型
- デコレータ: クラスデコレータ、メソッドデコレータ
- React + TypeScript: ReactでのTypeScript活用
- Node.js + TypeScript: バックエンド開発
- 型定義ファイル: @typesパッケージ、d.tsファイルの作成
TypeScriptは、モダンなJavaScript開発において必須のスキルです。継続的な学習と実践を通じて、型安全で保守性の高いコードを書けるようになりましょう!
Happy Typing!
コメント
コメントを読み込み中...
