はじめに
前回の記事ではJavaScriptのモジュールシステムとViteを使ったフロントエンド開発環境の整え方を解説しました。Viteを使うとTypeScriptも設定なしで使えることに触れましたが、今回はそのTypeScriptを改めて取り上げます。「JavaScriptに型を付ける」というTypeScriptの基本的な考え方から、型注釈・インターフェース・型推論・ユニオン型・ジェネリクスまでを実践的なコード例で解説します。これまでの連載で書いてきたJavaScriptコードを型安全にする視点でお届けします。
1. TypeScriptとは何か
1.1 JavaScriptとの関係
TypeScriptはMicrosoftが開発したJavaScriptのスーパーセットです。TypeScriptで書いたコードはコンパイルして(または変換して)JavaScriptになります。
TypeScript(.ts)
↓ コンパイル(tscまたはVite・esbuildなど)
JavaScript(.js)
↓ ブラウザやNode.jsで実行
1.2 TypeScriptを使うメリット
| メリット | 内容 |
|---|---|
| 型エラーを事前に発見できる | 実行前にコードの問題を検出できる |
| エディタの補完が強力になる | VSCodeで型に基づいた自動補完が使える |
| コードの意図が伝わりやすい | 引数や戻り値の型がドキュメント代わりになる |
| リファクタリングが安全になる | 型チェックにより変更の影響範囲を把握できる |
1.3 ViteでTypeScriptを使い始める
前回の記事でViteプロジェクトを作成する際、バリアントで「TypeScript」を選ぶだけで即座に使えます。
bash
# 既存のViteプロジェクトにTypeScriptを追加する場合
npm install -D typescript
# tsconfig.jsonを生成する
npx tsc --init
json
// tsconfig.json(基本設定)
{
"compilerOptions": {
"target": "ES2020", // 出力するJSのバージョン
"module": "ESNext", // モジュール形式
"moduleResolution": "bundler", // バンドラー向けの解決方法
"strict": true, // 厳格な型チェックを有効にする
"noImplicitAny": true, // 暗黙のany型を禁止する
"skipLibCheck": true, // ライブラリの型チェックをスキップする
"paths": {
"@/*": ["./src/*"] // パスエイリアス
}
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
💡 strict: true から始めることを強く推奨します: 厳格モードは最初は窮屈に感じますが、バグの多くを事前に防いでくれます。最初から true にしておくと後から有効化するよりも対応が楽です。
2. 基本的な型注釈
2.1 プリミティブ型
typescript
// 変数に型を付ける
let username: string = '山田太郎';
let age: number = 30;
let isActive: boolean = true;
let nullValue: null = null;
let undefinedValue: undefined = undefined;
// 型注釈なしでも型推論で型が決まる
let inferred = '型推論で string になる'; // string型として扱われる
let count = 0; // number型として推論される
// ❌ 型が合わないと型エラーになる
let name: string = 42; // Error: Type 'number' is not assignable to type 'string'
2.2 配列型
// 配列の型注釈
let tags: string[] = ['CSS', 'JavaScript', 'WordPress'];
let scores: number[] = [95, 87, 100];
let flags: boolean[] = [true, false, true];
// Array<T>という書き方もある(同じ意味)
let tags2: Array<string> = ['CSS', 'JavaScript'];
// タプル型(要素数と各要素の型を固定する)
let coordinate: [number, number] = [35.6895, 139.6917];
let nameAndAge: [string, number] = ['山田太郎', 30];
2.3 関数の型注釈
// 引数と戻り値に型を付ける
function add( a: number, b: number ): number {
return a + b;
}
// アロー関数
const multiply = ( a: number, b: number ): number => a * b;
// オプショナル引数(?を付けると省略可能になる)
function greet( name: string, title?: string ): string {
return title ? `${title} ${name}さん` : `${name}さん`;
}
greet( '山田太郎' ); // OK(titleは省略可能)
greet( '山田太郎', '様' ); // OK
// デフォルト引数
function createUser( name: string, role: string = 'viewer' ): object {
return { name, role };
}
// 戻り値がない関数はvoidを使う
function logMessage( message: string ): void {
console.log( message );
}
// 決して戻らない関数(エラーを投げる・無限ループ)はnever
function throwError( message: string ): never {
throw new Error( message );
}
3. オブジェクト型とインターフェース
3.1 インラインでオブジェクトの型を定義する
// オブジェクトの型をインラインで定義する
function displayUser( user: { name: string; age: number; email: string } ): void {
console.log( `${user.name}(${user.age}歳):${user.email}` );
}
// 使い回す場合はインターフェースで定義する
3.2 interfaceで型を定義する
// interfaceでオブジェクトの型を定義する
interface User {
id: number;
name: string;
email: string;
age?: number; // ?を付けるとオプショナル(省略可能)
readonly createdAt: string; // readonlyを付けると変更不可
}
// interfaceを使って変数に型を付ける
const user: User = {
id: 1,
name: '山田太郎',
email: 'yamada@example.com',
createdAt: '2025-01-01',
};
// ageはオプショナルなので省略できる
const userWithoutAge: User = {
id: 2,
name: '鈴木花子',
email: 'suzuki@example.com',
createdAt: '2025-02-01',
};
// readonlyは変更しようとするとエラーになる
user.createdAt = '2025-12-31'; // Error: Cannot assign to 'createdAt' because it is a read-only property
3.3 interfaceの拡張
// interfaceはextendsで拡張できる
interface Person {
name: string;
email: string;
}
interface Employee extends Person {
employeeId: string;
department: string;
startDate: string;
}
interface Admin extends Employee {
permissions: string[];
}
const admin: Admin = {
name: '田中一郎',
email: 'tanaka@example.com',
employeeId: 'EMP001',
department: 'エンジニアリング',
startDate: '2023-04-01',
permissions: ['user_manage', 'content_manage'],
};
3.4 typeエイリアスとの違い
// typeエイリアス(interfaceとよく似ている)
type Point = {
x: number;
y: number;
};
// typeはユニオン型・インターセクション型などにも使える
type StringOrNumber = string | number;
type ID = string | number;
// interfaceはオブジェクト型の定義に向いている
// typeはプリミティブ型の別名やユニオン型に向いている
// interfaceはマージできる(同じ名前で追加定義できる)
interface Config {
theme: string;
}
interface Config {
language: string;
}
// → { theme: string; language: string; } として扱われる
💡 オブジェクトの形を定義するときは interface・それ以外は type が一般的な使い分けです: どちらでも書けるケースは多いですが、チームで統一することが大切です。Reactのコンポーネントpropsには interface が使われることが多いです。
4. ユニオン型とリテラル型
4.1 ユニオン型(|)
// どちらかの型を受け付ける
type StringOrNumber = string | number;
function printId( id: StringOrNumber ): void {
if ( typeof id === 'string' ) {
console.log( `文字列のID: ${id.toUpperCase()}` );
} else {
console.log( `数値のID: ${id.toFixed()}` );
}
}
printId( 'abc-123' ); // 文字列のID: ABC-123
printId( 42 ); // 数値のID: 42
// nullableな型(nullまたはundefinedを許容する)
type NullableString = string | null;
type MaybeNumber = number | undefined;
function findUser( id: number ): User | null {
// 見つからない場合はnullを返す
return null;
}
4.2 リテラル型
// 特定の値のみを許容する型
type Theme = 'light' | 'dark' | 'system';
type Status = 'pending' | 'fulfilled' | 'rejected';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type Direction = 'top' | 'right' | 'bottom' | 'left';
// リテラル型を引数に使う
function setTheme( theme: Theme ): void {
document.documentElement.setAttribute( 'data-theme', theme );
}
setTheme( 'dark' ); // OK
setTheme( 'blue' ); // Error: Argument of type '"blue"' is not assignable to type 'Theme'
// 数値のリテラル型
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;
type ZeroToHundred = 0 | 25 | 50 | 75 | 100; // 離散値
4.3 判別可能なユニオン型(Discriminated Union)
// 共通のプロパティで型を判別する
interface SuccessResponse {
status: 'success'; // リテラル型で判別する
data: unknown;
message: string;
}
interface ErrorResponse {
status: 'error'; // リテラル型で判別する
errorCode: number;
message: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse( response: ApiResponse ): void {
// statusの値でTypeScriptが型を絞り込む(型ガード)
if ( response.status === 'success' ) {
console.log( response.data ); // SuccessResponseとして扱われる
} else {
console.log( response.errorCode ); // ErrorResponseとして扱われる
}
}
5. ジェネリクス
5.1 ジェネリクスの基本
// 型を引数のように受け取れる(ジェネリクス)
// ❌ 型が固定されてしまう
function getFirst_string( arr: string[] ): string {
return arr[0];
}
function getFirst_number( arr: number[] ): number {
return arr[0];
}
// ✅ ジェネリクスを使うと型を汎用化できる
function getFirst<T>( arr: T[] ): T {
return arr[0];
}
const firstString = getFirst( ['CSS', 'JS', 'TS'] ); // string型と推論される
const firstNumber = getFirst( [1, 2, 3] ); // number型と推論される
const firstUser = getFirst<User>( users ); // User型
5.2 前回のコードをジェネリクスで型付けする
// storage.tsのgetメソッドをジェネリクスで型安全にする
interface StorageWrapper {
set<T>( key: string, value: T ): boolean;
get<T>( key: string, defaultValue?: T ): T | null;
remove( key: string ): void;
has( key: string ): boolean;
}
const storage: StorageWrapper = {
set<T>( key: string, value: T ): boolean {
try {
localStorage.setItem( key, JSON.stringify( value ) );
return true;
} catch {
return false;
}
},
get<T>( key: string, defaultValue: T | null = null ): T | null {
try {
const item = localStorage.getItem( key );
if ( item === null ) return defaultValue;
return JSON.parse( item ) as T;
} catch {
return defaultValue;
}
},
remove( key: string ): void {
localStorage.removeItem( key );
},
has( key: string ): boolean {
return localStorage.getItem( key ) !== null;
},
};
// 使い方(型が安全になる)
interface UserPreference {
theme: string;
language: string;
fontSize: number;
}
storage.set<UserPreference>( 'pref', { theme: 'dark', language: 'ja', fontSize: 16 } );
const pref = storage.get<UserPreference>( 'pref' ); // UserPreference | null として型付けられる
5.3 非同期関数のジェネリクス
// apiRequest関数を型安全にする
async function apiRequest<T>( endpoint: string, options: RequestInit = {} ): Promise<T> {
const response = await fetch( `https://api.example.com${endpoint}`, options );
if ( !response.ok ) {
throw new Error( `HTTP Error: ${response.status}` );
}
return response.json() as Promise<T>;
}
// 使い方
interface Post {
id: number;
title: string;
body: string;
}
const posts = await apiRequest<Post[]>( '/posts' ); // Post[]型
const post = await apiRequest<Post>( '/posts/1' ); // Post型
// TypeScriptがプロパティの補完を提供してくれる
console.log( post.title ); // 補完が効く
console.log( post.body ); // 補完が効く
6. 型ユーティリティ
6.1 TypeScript組み込みのユーティリティ型
interface User {
id: number;
name: string;
email: string;
password: string;
role: 'admin' | 'editor' | 'viewer';
}
// Partial:すべてのプロパティをオプショナルにする
type PartialUser = Partial<User>;
// → { id?: number; name?: string; email?: string; ... }
// Required:すべてのプロパティを必須にする
type RequiredUser = Required<PartialUser>;
// Readonly:すべてのプロパティを読み取り専用にする
type ReadonlyUser = Readonly<User>;
// Pick:特定のプロパティだけを抽出する
type PublicUser = Pick<User, 'id' | 'name' | 'role'>;
// → { id: number; name: string; role: 'admin' | 'editor' | 'viewer' }
// Omit:特定のプロパティを除外する
type SafeUser = Omit<User, 'password'>;
// → { id: number; name: string; email: string; role: ... }
// Record:キーと値の型を指定したオブジェクト型を作る
type ThemeColors = Record<string, string>;
const colors: ThemeColors = {
primary: '#1A5FC0',
secondary: '#27AE60',
};
// ReturnType:関数の戻り値の型を取得する
function createUser() {
return { id: 1, name: '山田太郎' };
}
type CreatedUser = ReturnType<typeof createUser>;
// → { id: number; name: string }
7. 実践:連載のコードをTypeScript化する
7.1 フォームバリデーションを型安全にする
// validation.ts
// バリデーターの型
type Validator = ( value: string ) => string | null;
// バリデーションルール
interface ValidationRule {
validator: Validator;
message?: string;
}
// フィールドの設定
interface FieldConfig {
rules: Validator[];
}
// バリデーション結果
interface ValidationResult {
isValid: boolean;
errors: Record<string, string[]>;
}
// validators
const validators = {
required: (): Validator => ( value: string ) =>
value.trim() !== '' ? null : 'この項目は必須です。',
email: (): Validator => ( value: string ) => {
if ( !value.trim() ) return null;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( value )
? null
: '正しいメールアドレスを入力してください。';
},
minLength: ( min: number ): Validator => ( value: string ) => {
if ( !value.trim() ) return null;
return value.length >= min ? null : `${min}文字以上で入力してください。`;
},
maxLength: ( max: number ): Validator => ( value: string ) =>
value.length <= max ? null : `${max}文字以内で入力してください。`,
};
// フォーム全体のバリデーション
function validateForm(
values: Record<string, string>,
configs: Record<string, FieldConfig>
): ValidationResult {
const errors: Record<string, string[]> = {};
let isValid = true;
for ( const [field, config] of Object.entries( configs ) ) {
const value = values[field] ?? '';
const fieldErrors: string[] = [];
for ( const rule of config.rules ) {
const error = rule( value );
if ( error ) fieldErrors.push( error );
}
if ( fieldErrors.length > 0 ) {
errors[field] = fieldErrors;
isValid = false;
}
}
return { isValid, errors };
}
export { validators, validateForm };
export type { ValidationResult, FieldConfig };
7.2 型安全なAPIクライアント
// api/client.ts
interface ApiConfig {
baseUrl: string;
timeout?: number;
headers?: Record<string, string>;
}
interface ApiError extends Error {
status: number;
response: Response;
}
class ApiClient {
private config: Required<ApiConfig>;
constructor( config: ApiConfig ) {
this.config = {
baseUrl: config.baseUrl,
timeout: config.timeout ?? 10000,
headers: config.headers ?? {},
};
}
private async request<T>( endpoint: string, options: RequestInit = {} ): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout( () => controller.abort(), this.config.timeout );
try {
const response = await fetch(
`${this.config.baseUrl}${endpoint}`,
{
...options,
headers: {
'Content-Type': 'application/json',
...this.config.headers,
...options.headers,
},
signal: controller.signal,
}
);
if ( !response.ok ) {
const error = new Error( `HTTP Error: ${response.status}` ) as ApiError;
error.status = response.status;
error.response = response;
throw error;
}
if ( response.status === 204 ) return null as unknown as T;
return response.json() as Promise<T>;
} finally {
clearTimeout( timeoutId );
}
}
get<T>( endpoint: string ): Promise<T> {
return this.request<T>( endpoint );
}
post<T>( endpoint: string, data: unknown ): Promise<T> {
return this.request<T>( endpoint, {
method: 'POST',
body: JSON.stringify( data ),
} );
}
put<T>( endpoint: string, data: unknown ): Promise<T> {
return this.request<T>( endpoint, {
method: 'PUT',
body: JSON.stringify( data ),
} );
}
delete<T>( endpoint: string ): Promise<T> {
return this.request<T>( endpoint, { method: 'DELETE' } );
}
}
export default ApiClient;
export type { ApiConfig, ApiError };
8. 実践チェックリスト
✓ tsconfig.json で strict: true を設定しているか
✓ 関数の引数と戻り値に型注釈を付けているか
✓ オブジェクトの形は interface で定義しているか
✓ null や undefined の可能性がある値に | null または | undefined を付けているか
✓ リテラル型でテーマ・ステータスなどの定数値を型として定義しているか
✓ 汎用的な関数にはジェネリクス <T> を使って型を汎用化しているか
✓ Partial・Pick・Omit などのユーティリティ型を活用しているか
✓ APIレスポンスの型を interface で定義してジェネリクスに渡しているか
✓ VSCodeでエラーの波線がなくなっているか(型エラーがないか)
まとめ
今回はTypeScriptの基本的な型注釈からインターフェース・ジェネリクス・型ユーティリティまでを解説しました。ポイントをまとめると:
- TypeScriptはJavaScriptのスーパーセットでコンパイルするとJavaScriptになる
strict: trueから始めると多くのバグを事前に防げるinterfaceはオブジェクトの形を定義するのに使いextendsで拡張できるtypeはプリミティブ型の別名やユニオン型に向いている- リテラル型でテーマ・ステータスなどの限定値を型として表現できる
- ジェネリクス
<T>を使うと型を汎用化して安全な再利用可能コードを書ける Partial・Pick・Omit・Recordなどのユーティリティ型で型を加工できる- 連載で書いてきたAPIクライアント・バリデーション・ストレージも型安全にできる
次の記事では、ReactとViteを組み合わせてコンポーネントベースのUI開発を始める方法を解説します。お楽しみに!
コメント