TypeScript習得ロードマップ2026:型安全な開発者への道


はじめに:なぜ2026年にTypeScriptが必須なのか

TypeScriptは2026年現在、フロントエンド・バックエンドを問わず、JavaScript開発のデファクトスタンダードとなっています。

State of JS 2025の調査結果によると、TypeScriptの使用率は93%を超え、新規プロジェクトでTypeScriptを採用しない理由はほぼなくなりました(参照: State of JavaScript Survey https://stateofjs.com/)。

TypeScriptを学ぶべき理由

理由詳細
バグの早期発見コンパイル時に型エラーを検出し、実行時エラーを大幅に削減
コード補完の強化IDEが型情報を利用して正確な補完を提供
リファクタリングの安全性型があるため、変更箇所を正確に把握できる
ドキュメントとしての型型定義がそのままAPIのドキュメントになる
求人の必須スキルフロントエンド求人の約65%がTypeScript必須

この記事の対象読者

  • JavaScriptの基本文法を理解している
  • ES6+の構文(アロー関数、分割代入、スプレッド構文等)を使ったことがある
  • TypeScriptに興味があるが、どこから始めればいいかわからない

Phase 1: 型システムの基礎(1-2週間)

基本型(Primitive Types)

TypeScriptの型システムはJavaScriptのプリミティブ型を基盤としています。

// 基本的な型注釈
const name: string = '田中太郎';
const age: number = 30;
const isActive: boolean = true;
const value: null = null;
const notDefined: undefined = undefined;

// 型推論を活用する(明示的な型注釈は不要な場合が多い)
const greeting = 'こんにちは'; // string と推論される
const count = 42;              // number と推論される
const enabled = true;          // boolean と推論される

// 関数の型注釈
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 {
  if (title) {
    return `${title} ${name}さん、こんにちは`;
  }
  return `${name}さん、こんにちは`;
}

// デフォルト値
function createUser(name: string, role: string = 'user'): { name: string; role: string } {
  return { name, role };
}

つまずきポイント1: anyの誘惑

初心者が最もやりがちな間違いは、型エラーが出るたびに any を使ってしまうことです。

// NG: anyを使って型チェックを回避する
function processData(data: any): any {
  return data.items.map((item: any) => item.name);
}

// OK: 適切な型を定義する
interface DataResponse {
  items: Array<{
    id: number;
    name: string;
  }>;
}

function processData(data: DataResponse): string[] {
  return data.items.map(item => item.name);
}

any を使うとTypeScriptを導入する意味がなくなります。どうしても型がわからない場合は unknown を使い、型ガードで絞り込んでください。

// unknownと型ガードの組み合わせ
function safeParseJSON(jsonString: string): unknown {
  try {
    return JSON.parse(jsonString);
  } catch {
    return null;
  }
}

// 型ガード関数
function isStringArray(value: unknown): value is string[] {
  return (
    Array.isArray(value) &&
    value.every(item => typeof item === 'string')
  );
}

const parsed = safeParseJSON('["a", "b", "c"]');
if (isStringArray(parsed)) {
  // ここではparsedはstring[]として扱える
  console.log(parsed.join(', '));
}

配列とタプル

// 配列
const numbers: number[] = [1, 2, 3, 4, 5];
const names: Array<string> = ['田中', '佐藤', '鈴木'];

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

// タプル(固定長・固定型の配列)
const pair: [string, number] = ['田中', 30];
const [pairName, pairAge] = pair;

// ラベル付きタプル(可読性が向上)
type UserEntry = [name: string, age: number, active: boolean];
const entry: UserEntry = ['田中', 30, true];

// 可変長タプル
type StringsAndNumber = [...string[], number];
const data: StringsAndNumber = ['a', 'b', 'c', 42];

オブジェクト型とインターフェース

// インターフェースの定義
interface User {
  id: number;
  name: string;
  email: string;
  age?: number;              // オプショナルプロパティ
  readonly createdAt: Date;  // 読み取り専用
}

// typeエイリアスとの違い
type UserType = {
  id: number;
  name: string;
  email: string;
};

// interfaceは拡張(extends)が可能
interface AdminUser extends User {
  permissions: string[];
  department: string;
}

// typeはユニオン型やインターセクション型が使える
type Status = 'active' | 'inactive' | 'suspended';
type UserWithStatus = User & { status: Status };

// インデックスシグネチャ
interface Dictionary {
  [key: string]: string;
}

const translations: Dictionary = {
  hello: 'こんにちは',
  goodbye: 'さようなら'
};

// Record型を使った同等の表現
const config: Record<string, string | number | boolean> = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  debug: false
};

つまずきポイント2: interfaceとtypeの使い分け

// interface: オブジェクト型の定義に使う。拡張(extends)が得意
interface Animal {
  name: string;
  sound(): string;
}

interface Dog extends Animal {
  breed: string;
  fetch(): void;
}

// interfaceは同名で宣言マージができる(ライブラリの型拡張に便利)
interface Window {
  myCustomProperty: string;
}

// type: ユニオン型、インターセクション型、マップ型など複雑な型に使う
type Result<T> = { success: true; data: T } | { success: false; error: string };
type EventName = 'click' | 'hover' | 'focus';
type Nullable<T> = T | null;

一般的なガイドラインとして、オブジェクトの形状を定義するときは interface、それ以外は type を使うことが推奨されています(参照: TypeScript公式ドキュメント https://www.typescriptlang.org/docs/handbook/2/everyday-types.html)。


Phase 2: ユニオン型とリテラル型(1-2週間)

ユニオン型の基本

// 文字列リテラル型のユニオン
type Direction = 'north' | 'south' | 'east' | 'west';

function move(direction: Direction, distance: number): string {
  return `${direction}に${distance}m移動`;
}

move('north', 10);  // OK
// move('up', 10);  // エラー: 'up'はDirection型に割り当てられません

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

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

// 判別可能なユニオン型(Discriminated Union)
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number };

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
  }
}

// TypeScriptが各caseで正しい型に絞り込んでくれる
const circle: Shape = { kind: 'circle', radius: 5 };
console.log(calculateArea(circle)); // 78.54...

型の絞り込み(Type Narrowing)

// typeof による絞り込み
function padLeft(value: string | number, padding: string | number): string {
  if (typeof padding === 'number') {
    return ' '.repeat(padding) + value;
  }
  return padding + value;
}

// in 演算子による絞り込み
interface Fish {
  swim(): void;
}

interface Bird {
  fly(): void;
}

function move(animal: Fish | Bird): void {
  if ('swim' in animal) {
    animal.swim(); // Fish として扱われる
  } else {
    animal.fly();  // Bird として扱われる
  }
}

// instanceof による絞り込み
function processDate(input: string | Date): string {
  if (input instanceof Date) {
    return input.toLocaleDateString('ja-JP');
  }
  return new Date(input).toLocaleDateString('ja-JP');
}

// カスタム型ガード
interface APIError {
  code: number;
  message: string;
}

function isAPIError(value: unknown): value is APIError {
  return (
    typeof value === 'object' &&
    value !== null &&
    'code' in value &&
    'message' in value &&
    typeof (value as APIError).code === 'number' &&
    typeof (value as APIError).message === 'string'
  );
}

async function fetchUser(id: number) {
  try {
    const response = await fetch(`/api/users/${id}`);
    const data: unknown = await response.json();

    if (isAPIError(data)) {
      console.error(`APIエラー: ${data.code} - ${data.message}`);
      return null;
    }

    return data as User;
  } catch (error) {
    console.error('通信エラー:', error);
    return null;
  }
}

つまずきポイント3: nullとundefinedの扱い

// strictNullChecksが有効な場合(推奨)
function getLength(str: string | null): number {
  // str.length; // エラー: strはnullの可能性がある

  // 方法1: if文でチェック
  if (str !== null) {
    return str.length;
  }
  return 0;

  // 方法2: オプショナルチェーン + Nullish Coalescing
  // return str?.length ?? 0;
}

// Non-null assertion(!演算子)は最終手段
function processElement(id: string): void {
  const element = document.getElementById(id);
  // element!.textContent = 'hello'; // 危険: elementがnullなら実行時エラー

  // 安全な方法
  if (element) {
    element.textContent = 'hello';
  }
}

Phase 3: ジェネリクス(2-3週間)

ジェネリクスはTypeScriptの中で最も強力で、同時に最もつまずきやすい概念です。

ジェネリクスの基本

// ジェネリクスなし: 型ごとに関数を作る必要がある
function getFirstString(arr: string[]): string | undefined {
  return arr[0];
}
function getFirstNumber(arr: number[]): number | undefined {
  return arr[0];
}

// ジェネリクスあり: 1つの関数で全ての型に対応
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0];
}

const firstStr = getFirst(['a', 'b', 'c']); // string | undefined
const firstNum = getFirst([1, 2, 3]);        // number | undefined

// 複数の型パラメータ
function zip<T, U>(arr1: T[], arr2: U[]): [T, U][] {
  const length = Math.min(arr1.length, arr2.length);
  const result: [T, U][] = [];
  for (let i = 0; i < length; i++) {
    result.push([arr1[i], arr2[i]]);
  }
  return result;
}

const zipped = zip(['a', 'b', 'c'], [1, 2, 3]);
// [string, number][] = [['a', 1], ['b', 2], ['c', 3]]

ジェネリクスの制約(Constraints)

// extends で型パラメータに制約を付ける
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(value: T): T {
  console.log(`長さ: ${value.length}`);
  return value;
}

logLength('hello');        // OK: stringはlengthを持つ
logLength([1, 2, 3]);     // OK: arrayは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: 30, email: 'tanaka@example.com' };
const userName = getProperty(user, 'name');  // string
const userAge = getProperty(user, 'age');    // number
// getProperty(user, 'phone');               // エラー: 'phone'は存在しない

ジェネリクスの実践パターン

// パターン1: APIレスポンスの型安全なラッパー
interface APIResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: string;
}

interface PaginatedResponse<T> extends APIResponse<T[]> {
  pagination: {
    page: number;
    perPage: number;
    total: number;
    totalPages: number;
  };
}

async function fetchAPI<T>(endpoint: string): Promise<APIResponse<T>> {
  const response = await fetch(`/api${endpoint}`);
  if (!response.ok) {
    throw new Error(`API Error: ${response.status}`);
  }
  return response.json();
}

// 使用例
interface User {
  id: number;
  name: string;
  email: string;
}

interface Post {
  id: number;
  title: string;
  content: string;
  authorId: number;
}

// 型が自動的に推論される
const userResponse = await fetchAPI<User>('/users/1');
console.log(userResponse.data.name); // string

const postsResponse = await fetchAPI<Post[]>('/posts');
console.log(postsResponse.data[0].title); // string

// パターン2: ジェネリックなリポジトリパターン
interface Repository<T extends { id: number }> {
  findById(id: number): Promise<T | null>;
  findAll(): Promise<T[]>;
  create(data: Omit<T, 'id'>): Promise<T>;
  update(id: number, data: Partial<Omit<T, 'id'>>): Promise<T>;
  delete(id: number): Promise<boolean>;
}

class InMemoryRepository<T extends { id: number }> implements Repository<T> {
  private items: T[] = [];
  private nextId = 1;

  async findById(id: number): Promise<T | null> {
    return this.items.find(item => item.id === id) ?? null;
  }

  async findAll(): Promise<T[]> {
    return [...this.items];
  }

  async create(data: Omit<T, 'id'>): Promise<T> {
    const item = { ...data, id: this.nextId++ } as T;
    this.items.push(item);
    return item;
  }

  async update(id: number, data: Partial<Omit<T, 'id'>>): Promise<T> {
    const index = this.items.findIndex(item => item.id === id);
    if (index === -1) throw new Error('Not found');
    this.items[index] = { ...this.items[index], ...data };
    return this.items[index];
  }

  async delete(id: number): Promise<boolean> {
    const index = this.items.findIndex(item => item.id === id);
    if (index === -1) return false;
    this.items.splice(index, 1);
    return true;
  }
}

// 使用例
const userRepo = new InMemoryRepository<User>();
await userRepo.create({ name: '田中', email: 'tanaka@example.com' });
const allUsers = await userRepo.findAll();

つまずきポイント4: ジェネリクスの型引数の推論

// 型引数は明示しなくても推論されることが多い
function identity<T>(value: T): T {
  return value;
}

// 型引数を省略しても正しく推論される
const str = identity('hello');  // T = string と推論
const num = identity(42);       // T = number と推論

// ただし、複雑なケースでは明示が必要
function createPair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

// 推論に任せる場合
const pair1 = createPair('hello', 42); // [string, number]

// 特定の型を強制する場合
const pair2 = createPair<string, string>('hello', 'world');

Phase 4: ユーティリティ型(1-2週間)

TypeScriptには組み込みのユーティリティ型が多数用意されています。これらを使いこなすことで、型定義の重複を減らし、保守性の高いコードが書けます。

主要なユーティリティ型

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  role: 'admin' | 'user' | 'viewer';
  createdAt: Date;
}

// Partial<T>: 全プロパティをオプショナルにする
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; ... }

// Required<T>: 全プロパティを必須にする
type RequiredUser = Required<User>;

// Readonly<T>: 全プロパティを読み取り専用にする
type ImmutableUser = Readonly<User>;

// Pick<T, K>: 特定のプロパティだけを抽出する
type UserPreview = Pick<User, 'id' | 'name' | 'role'>;
// { id: number; name: string; role: 'admin' | 'user' | 'viewer' }

// Omit<T, K>: 特定のプロパティを除外する
type UserInput = Omit<User, 'id' | 'createdAt'>;
// { name: string; email: string; age: number; role: ... }

// Record<K, V>: キーと値の型を指定したオブジェクト
type RolePermissions = Record<User['role'], string[]>;
const permissions: RolePermissions = {
  admin: ['read', 'write', 'delete', 'manage_users'],
  user: ['read', 'write'],
  viewer: ['read']
};

// Exclude<T, U>: ユニオン型から特定の型を除外
type NonAdminRole = Exclude<User['role'], 'admin'>; // 'user' | 'viewer'

// Extract<T, U>: ユニオン型から特定の型を抽出
type AdminRole = Extract<User['role'], 'admin'>; // 'admin'

// NonNullable<T>: nullとundefinedを除外
type NullableString = string | null | undefined;
type DefiniteString = NonNullable<NullableString>; // string

// ReturnType<T>: 関数の戻り値の型を取得
function fetchUserData() {
  return { users: [] as User[], total: 0 };
}
type FetchResult = ReturnType<typeof fetchUserData>;
// { users: User[]; total: number }

// Parameters<T>: 関数のパラメータ型をタプルとして取得
function createUser(name: string, email: string, age: number): User {
  return { id: 1, name, email, age, role: 'user', createdAt: new Date() };
}
type CreateUserParams = Parameters<typeof createUser>;
// [string, string, number]

// Awaited<T>: Promiseのアンラップ
type PromiseResult = Awaited<Promise<string>>; // string
type NestedPromise = Awaited<Promise<Promise<number>>>; // number

ユーティリティ型の実践的な組み合わせ

// CRUD操作の型定義
interface Entity {
  id: number;
  createdAt: Date;
  updatedAt: Date;
}

interface Article extends Entity {
  title: string;
  content: string;
  authorId: number;
  tags: string[];
  published: boolean;
}

// 作成時: id, createdAt, updatedAtは自動生成
type CreateArticleInput = Omit<Article, keyof Entity>;

// 更新時: 全フィールドオプショナル(id等は除外)
type UpdateArticleInput = Partial<Omit<Article, keyof Entity>>;

// 一覧表示用: コンテンツは不要
type ArticleListItem = Pick<Article, 'id' | 'title' | 'authorId' | 'tags' | 'published' | 'createdAt'>;

// API エンドポイントの型定義
interface APIEndpoints {
  'GET /articles': {
    response: ArticleListItem[];
    query: { page?: number; limit?: number; tag?: string };
  };
  'GET /articles/:id': {
    response: Article;
    params: { id: number };
  };
  'POST /articles': {
    response: Article;
    body: CreateArticleInput;
  };
  'PATCH /articles/:id': {
    response: Article;
    params: { id: number };
    body: UpdateArticleInput;
  };
  'DELETE /articles/:id': {
    response: void;
    params: { id: number };
  };
}

Phase 5: 高度な型パターン(2-3週間)

Mapped Types(マップ型)

// 全プロパティの型を変換する
type Stringify<T> = {
  [K in keyof T]: string;
};

interface Config {
  port: number;
  host: string;
  debug: boolean;
}

type StringConfig = Stringify<Config>;
// { port: string; host: string; debug: string }

// 条件付きマップ型
type ConditionalPick<T, Condition> = {
  [K in keyof T as T[K] extends Condition ? K : never]: T[K];
};

type StringProps = ConditionalPick<Config, string>;
// { host: string }

type NumberProps = ConditionalPick<Config, number>;
// { port: number }

// Getterを自動生成する型
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type ConfigGetters = Getters<Config>;
// { getPort: () => number; getHost: () => string; getDebug: () => boolean }

Conditional Types(条件付き型)

// 条件付き型の基本
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

// 配列の要素型を取得する
type ElementType<T> = T extends (infer E)[] ? E : never;

type StringElement = ElementType<string[]>;  // string
type NumberElement = ElementType<number[]>;  // number

// 関数の引数型を取得する
type FirstArgument<T> = T extends (arg: infer A, ...args: unknown[]) => unknown ? A : never;

type Arg = FirstArgument<(name: string, age: number) => void>; // string

// Promiseのアンラップ(自作版)
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;

type Result1 = UnwrapPromise<Promise<string>>;            // string
type Result2 = UnwrapPromise<Promise<Promise<number>>>;   // number

// 実用的な条件付き型: APIレスポンスの正規化
type NormalizeResponse<T> = T extends Array<infer Item>
  ? { items: Item[]; count: number }
  : { item: T };

type UserResponse = NormalizeResponse<User>;     // { item: User }
type UsersResponse = NormalizeResponse<User[]>;  // { items: User[]; count: number }

Template Literal Types

// テンプレートリテラル型
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type APIPath = '/users' | '/posts' | '/comments';

type Endpoint = `${HTTPMethod} ${APIPath}`;
// 'GET /users' | 'GET /posts' | 'GET /comments' | 'POST /users' | ...

// イベントハンドラの型
type EventName = 'click' | 'hover' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<EventName>}`;
// 'onClick' | 'onHover' | 'onFocus' | 'onBlur'

// CSSプロパティ風の型
type CSSUnit = 'px' | 'rem' | 'em' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;

function setWidth(value: CSSValue): void {
  console.log(`width: ${value}`);
}

setWidth('100px');   // OK
setWidth('2.5rem');  // OK
// setWidth('100');  // エラー: 単位が必要

Phase 6: 実践パターン集(2-3週間)

パターン1: 型安全なイベントエミッター

type EventMap = {
  userLogin: { userId: number; timestamp: Date };
  userLogout: { userId: number };
  pageView: { path: string; referrer?: string };
  error: { message: string; code: number };
};

class TypedEventEmitter<T extends Record<string, unknown>> {
  private listeners: {
    [K in keyof T]?: Array<(data: T[K]) => void>;
  } = {};

  on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
  }

  off<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
    const listeners = this.listeners[event];
    if (!listeners) return;
    this.listeners[event] = listeners.filter(l => l !== listener);
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    const listeners = this.listeners[event];
    if (!listeners) return;
    listeners.forEach(listener => listener(data));
  }
}

// 使用例
const emitter = new TypedEventEmitter<EventMap>();

emitter.on('userLogin', (data) => {
  console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});

emitter.emit('userLogin', {
  userId: 123,
  timestamp: new Date()
});

// 型エラー: propertyが不足
// emitter.emit('userLogin', { userId: 123 });

パターン2: Builderパターン

interface QueryConfig {
  table: string;
  columns: string[];
  where: Array<{ column: string; operator: string; value: unknown }>;
  orderBy?: { column: string; direction: 'ASC' | 'DESC' };
  limit?: number;
  offset?: number;
}

class QueryBuilder {
  private config: QueryConfig;

  constructor(table: string) {
    this.config = {
      table,
      columns: ['*'],
      where: []
    };
  }

  select(...columns: string[]): this {
    this.config.columns = columns;
    return this;
  }

  where(column: string, operator: string, value: unknown): this {
    this.config.where.push({ column, operator, value });
    return this;
  }

  orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
    this.config.orderBy = { column, direction };
    return this;
  }

  limit(count: number): this {
    this.config.limit = count;
    return this;
  }

  offset(count: number): this {
    this.config.offset = count;
    return this;
  }

  build(): string {
    let query = `SELECT ${this.config.columns.join(', ')} FROM ${this.config.table}`;

    if (this.config.where.length > 0) {
      const conditions = this.config.where
        .map(w => `${w.column} ${w.operator} ?`)
        .join(' AND ');
      query += ` WHERE ${conditions}`;
    }

    if (this.config.orderBy) {
      query += ` ORDER BY ${this.config.orderBy.column} ${this.config.orderBy.direction}`;
    }

    if (this.config.limit !== undefined) {
      query += ` LIMIT ${this.config.limit}`;
    }

    if (this.config.offset !== undefined) {
      query += ` OFFSET ${this.config.offset}`;
    }

    return query;
  }
}

// 使用例(メソッドチェーン)
const query = new QueryBuilder('users')
  .select('id', 'name', 'email')
  .where('age', '>=', 18)
  .where('active', '=', true)
  .orderBy('name', 'ASC')
  .limit(20)
  .offset(0)
  .build();

console.log(query);
// SELECT id, name, email FROM users WHERE age >= ? AND active = ? ORDER BY name ASC LIMIT 20 OFFSET 0

パターン3: Result型(エラーハンドリング)

// 成功と失敗を型で表現する
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

// ヘルパー関数
function ok<T>(value: T): Result<T, never> {
  return { ok: true, value };
}

function err<E>(error: E): Result<never, E> {
  return { ok: false, error };
}

// 使用例: ユーザー入力のバリデーション
interface ValidationError {
  field: string;
  message: string;
}

function validateEmail(email: string): Result<string, ValidationError> {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    return err({ field: 'email', message: '有効なメールアドレスを入力してください' });
  }
  return ok(email);
}

function validateAge(age: number): Result<number, ValidationError> {
  if (age < 0 || age > 150) {
    return err({ field: 'age', message: '年齢は0-150の範囲で入力してください' });
  }
  return ok(age);
}

interface UserInput {
  name: string;
  email: string;
  age: number;
}

function validateUserInput(input: UserInput): Result<UserInput, ValidationError[]> {
  const errors: ValidationError[] = [];

  if (input.name.trim().length === 0) {
    errors.push({ field: 'name', message: '名前は必須です' });
  }

  const emailResult = validateEmail(input.email);
  if (!emailResult.ok) {
    errors.push(emailResult.error);
  }

  const ageResult = validateAge(input.age);
  if (!ageResult.ok) {
    errors.push(ageResult.error);
  }

  if (errors.length > 0) {
    return err(errors);
  }

  return ok(input);
}

// 呼び出し側
const result = validateUserInput({
  name: '田中太郎',
  email: 'tanaka@example.com',
  age: 30
});

if (result.ok) {
  console.log('バリデーション成功:', result.value);
} else {
  console.error('バリデーションエラー:', result.error);
}

tsconfig.jsonの推奨設定

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],

    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,

    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,

    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

参照: TypeScript公式ドキュメント「TSConfig Reference」(https://www.typescriptlang.org/tsconfig/)で各オプションの詳細を確認できます。

重要なのは "strict": true を最初から有効にすることです。後から有効にすると大量のエラーが発生するため、プロジェクト開始時に設定することを強く推奨します。


学習リソースまとめ

公式ドキュメント

リソースURL特徴
TypeScript Handbookhttps://www.typescriptlang.org/docs/handbook/公式。最も正確
TypeScript Playgroundhttps://www.typescriptlang.org/playブラウザで即座に試せる
Type Challengeshttps://github.com/type-challenges/type-challenges型パズルで実力を鍛える

書籍

書籍著者おすすめ度
プロを目指す人のためのTypeScript入門鈴木僚太最高(日本語で最も体系的)
Effective TypeScriptDan Vanderkam高(英語。中級者向け)
Programming TypeScriptBoris Cherny高(英語。網羅的)

学習の順序

TypeScript習得は以下の順序で進めてください。

1. 基本型とインターフェース(Phase 1: 1-2週間)

2. ユニオン型とリテラル型(Phase 2: 1-2週間)

3. ジェネリクス(Phase 3: 2-3週間)

4. ユーティリティ型(Phase 4: 1-2週間)

5. 高度な型パターン(Phase 5: 2-3週間)

6. 実践パターン集(Phase 6: 2-3週間)

合計で約9-15週間が目安です。


まとめ

TypeScriptの習得は、JavaScriptの知識がある状態からスタートすれば、体系的に学ぶことで3-4ヶ月で実務レベルに到達できます。

最も重要なのは以下の3点です。

  1. strict: trueを最初から有効にする: 甘い設定で始めると後で苦労する
  2. anyを使わない: unknownと型ガードで対応する習慣をつける
  3. 段階的に学ぶ: 基本型→ユニオン→ジェネリクス→ユーティリティ型の順序を守る

TypeScriptの型システムは奥が深いですが、実務で使うパターンは限られています。この記事で紹介したパターンを一つずつ実践し、手を動かしながら身につけてください。

参考文献:

🎨 プロクリエイターから学ぶオンライン動画講座【Coloso】
デザイン・イラスト・映像・3D・プログラミングなど、各分野のプロが直接教えるオンライン動画講座。韓国発グローバルプラットフォームで、実践的なスキルを身につけよう。
→ Colosoで講座を探す(無料会員登録)
---

関連記事