TypeScript 入門②

interface、type、enum、ジェネリクス、型の拡

TypeScript 基本型まとめ

前回の復習

  • プリミティブ型とオブジェクト型
  • ウニオン型と型推論
  • 関数の型定義

重要ポイント①: 型の種類

プリミティブ型

let name: string = "梨花";
let age: number = 11;
let isAlive: boolean = true;

配列とオブジェクト

let members: string[] = ["圭一", "レナ"];
let user: { name: string; age: number } = {
    name: "梨花", 
    age: 11
};

ウニオン型

let id: number | string = 123;
type Status = "pending" | "done";

特殊な型

let data: any = "危険"; // 使わない
let secret: unknown = "安全";
function log(): void { }
function error(): never { 
    throw new Error(); 
}

重要ポイント②: 実践的な使い方

型推論を活用

// 型を書かなくてもOK
let price = 1000;        // 自動的に number
let items = ["A", "B"];  // 自動的に string[]

型ガードで安全に

function process(value: string | number) {
    if (typeof value === "string") {
        return value.toUpperCase(); // string として使える
    }
    return value.toFixed(2);        // number として使える
}

type、interface

オブジェクト型を扱う方法

型エイリアス

型エイリアスは、型に別名をつけることができます

// オブジェクト型にエイリアスを付ける
type ClubMember = {
    name: string;
    age: number;
    weapon?: string; // オプショナルプロパティ
    curse: boolean;
};

let rena: ClubMember = {
    name: "竜宮レナ",
    age: 16,
    weapon: "鉈",
    curse: true
};

型エイリアス

型エイリアスは、オブジェクト型だけでなく、あらゆる型に名前を付けることができます

// プリミティブ型
type CharacterName = string;
type Age = number;
// ユニオン型
type Weapon = "鉈" | "バット" | "注射器" | "包丁";

// 関数型
type CurseFunction = (target: string) => boolean;
// 配列型
type CharacterList = string[];
type SuspicionLevels = number[];

// タプル型
type Coordinate = [number, number];

型エイリアス:拡張

型エイリアスは交差型「&」を使って拡張できます

// 基本の型
type Person = {
    name: string;
    age: number;
};

// 交差型で拡張
type ClubMember = Person & {
    weapon?: string;
    clubRole: string;
};
let mion: ClubMember = {
    name: "園崎魅音",    // Person より継承
    age: 16,            // Person より継承
    weapon: "エアガン",  // ClubMember 独自
    clubRole: "部長"     // ClubMember 独自
};
重要: 型エイリアスは作成後に変更できません

インターフェース

インターフェースはオブジェクトの構造を定義します

// 基本的なインターフェース
interface Character {
    name: string;
    age: number;
    isAlive: boolean;
    weapon?: string; // オプショナルプロパティ
}

let keiichi: Character = {
    name: "前原圭一",
    age: 16,
    isAlive: true
};

インターフェース:拡張

インターフェースは extends キーワードで他のインターフェースを拡張できます

interface Person {
    name: string;
    age: number;
}

interface ClubMember extends Person {
    weapon?: string;
    clubRole: string;
}

interface VillageElder extends Person {
    position: string;
    knowsSecret: boolean;
}
// 基本のインターフェース
 let mion: ClubMember = {
    name: "園崎魅音",        // Person より継承
    age: 16,                // Person より継承
    weapon: "エアガン",     // ClubMember 独自
    clubRole: "部長"        // ClubMember 独自
};

// extendsで拡張
let oryou: VillageElder = {
    name: "園崎お魎",        // Person より継承
    age: 78,                // Person より継承
    position: "当主",       // VillageElder 独自
    knowsSecret: true       // VillageElder 独自
};

interface vs type:新しいフィールドの追加

interface: 宣言マージ可能

interface ClubMember {
    name: string;
    age: number;
}

// 同じ名前で追加宣言可能
interface ClubMember {
    clubRole: string;
}

// 自動的にマージされる
let mion: ClubMember = {
    name: "園崎魅音",
    age: 16,
    clubRole: "部長"
};

type: 宣言マージ不可

type ClubMember = {
    name: string;
    age: number;
}

// エラー: 重複した識別子
// type ClubMember = {
//     weapon?: string;
//     clubRole: string;
// }

// 代わりに交差型を使用
type ExtendedClubMember = ClubMember & {
    weapon?: string;
    clubRole: string;
};
宣言マージ: interfaceは同じ名前で複数回宣言すると、自動的に1つのinterfaceにマージされます

理解度チェック①

以下のうち、型エイリアスでのみ定義可能なものはどれ?

// A
type Status = "active" | "inactive" | "pending";

// B
type Callback = (data: string) => void;
// C
type UserArray = User[];

// D
type UserTuple = [string, number, boolean];
A) ユニオン型のみ
B) 関数型のみ
C) 配列型のみ
D) すべて type でのみ定義可能
答え: D) interface はオブジェクトの形状のみ定義可能。ユニオン型、関数型、配列型、タプル型などは type でのみ定義できます。

理解度チェック②

エラーになるのはどれ?

// A
type Status = "loading" | "success" | "error";



// B  
type Handler = (event: Event) => void;

// C
type Config = {
  timeout: number;
};

type Config = {
  retries: number;
};
A) Status定義
B) Handler定義
C) Config重複
D) 全部正常
答え: C) typeは同名で複数定義できない!interfaceと違ってマージされずエラーになる。

理解度チェック③

以下のコードはどうなる?

interface Stand {
  name: string;
  power: number;
}

interface Stand {
  name: number;  // 型が違う!
  user: string;
}
A) 後の宣言が勝つ
B) コンパイルエラー
C) name: string | number になる
D) プロパティが削除される
答え: B) 同じプロパティ名で異なる型はマージできません。宣言マージには制約があります。

インターフェース vs 型エイリアス:使い分け

  • 型エイリアスは宣言マージに参加できないが、インターフェースは可能
  • インターフェースはオブジェクトの形状の宣言のみ、プリミティブ型の名前変更は不可
  • インターフェース名はエラーメッセージで元の形で表示される
  • インターフェースのextendsは、交差型よりもコンパイラーのパフォーマンスが良い場合が多い
どちらでも良いが、一貫性を保つことが重要。

インターフェース vs 型エイリアス:まとめ

比較項目 type interface
宣言マージ 不可(エラー) 可能
拡張方法 &(交差型) extends
定義できる型 プリミティブ・ユニオン・関数・条件型等 オブジェクト構造のみ
再定義 同名で定義不可 OK(マージ)
複雑な型の表現 条件型・ユニオン型等に強い 不向き
使用推奨 複雑・柔軟な型 標準的なオブジェクト
基本的な判断基準:迷った場合はinterfaceを使い、typeの特有機能が必要な時のみtypeを使用。

理解度チェック④

どの組み合わせが正しい?※複数回答

// A - Stand能力のID
"the-world" | "star-platinum" | "crazy-diamond";

// B - Stand情報のオブジェクト
{
  name: string;
  user: string;
  power: number;
}
A) 両方type
B) 両方interface
C) A=type, B=interface
D) A=interface, B=type
答え: A) と C) ユニオン型はtypeでしか作れない!オブジェクトはどちらでもOK。

理解度チェック⑤

どの構文が正しい?※複数回答

type Stand = {
  name: string;
  power: number;
};

interface Hamon {
  power: number;
}

type StandUser1 = Stand & { // A
  user: string;
};
type StandUser2 = Stand extends { // B
  user: string;
}

interface HamonUser1 & Hamon { // C
  user: string;
}

interface HamonUser2 extends Hamon { // D
  user: string;
}
A) StandUser1
B) StandUser2
C) HamonUser1
D) HamonUser2
答え: A) と D) interfaceは extends、typeは & で拡張できる

enum【列挙型】

  • 名前付き定数の集合を定義
  • 数値・文字列・異種混合の3タイプ
  • コンパイル時に型チェック、実行時に実際の値に変換

数値enum


// 明示的な数値
enum HttpStatus {
    OK = 200,
    NOT_FOUND = 404,
    SERVER_ERROR = 500
}

let response = HttpStatus.OK;
console.log(response);         // 200
console.log(HttpStatus[200]);  // "OK"

// 自動割り当て
enum StandType {
    STAR_PLATINUM,    // 0
    THE_WORLD,        // 1
    CRAZY_DIAMOND,    // 2
}

let jotaro = StandType.STAR_PLATINUM;
console.log(jotaro);        // 0
console.log(StandType[0]);  // "STAR_PLATINUM"
数値enumは双方向マッピングを提供。数値 → 名前、名前 → 数値 の逆引きが可能
デフォルトでは0から始まる連番が割り当てられる。

文字列enum


enum LogLevel {
    ERROR = "error",
    WARN = "warn",
    INFO = "info"
}

function log(level: LogLevel, message: string) {
    console.log(`[${level}] ${message}`);
}

log(LogLevel.ERROR, "something broke");
文字列enumは人間にとって読みやすい値を提供。 逆引きは不可能だが、デバッグ時に値が分かりやすい。

計算されたenum


enum Permission {
    READ = 1,      // 0001
    WRITE = 2,     // 0010
    EXECUTE = 4,   // 0100
    ALL = READ | WRITE | EXECUTE  // 0001 | 0010 | 0100 = 0111 = 7
}

function hasPermission(user: number, perm: Permission) {
    return (user & perm) === perm;
}

hasPermission(5, Permission.WRITE) // 5 = 0101 に WRITE(0010) は含まれない → false
ビットフラグパターンでよく使用。 計算された値は他のenum値や定数式から生成可能。

const assertion


// enum
enum Color { RED, GREEN, BLUE }

// const assertion
const Color2 = {
    RED: 'red',
    GREEN: 'green',
    BLUE: 'blue'
} as const;

type ColorType = typeof Color2[keyof typeof Color2];
    
enumのように使えるが、型だけを提供する。実行時の処理がなく、型チェックがより厳密。

まとめ

  • 定数管理: マジックナンバーを排除
  • 型安全性: コンパイル時エラー検出
  • 逆引き: 数値enumは双方向マッピング
  • 代替案: const assertionも検討

null と undefined

  • null: 意図的に「値がない」ことを示す
  • undefined: 変数が初期化されていない、またはプロパティが存在しない
  • どちらもプリミティブ型だが、意味が異なる
JavaScriptの歴史的な理由で両方存在する。混乱の元凶。

undefined が現れる場面


let userName; // 未初期化
console.log(userName); // undefined

function processUser(name?: string) {
  console.log(name); // 引数を渡さなければ undefined
}

const user: any = { name: "dio" };
console.log(user.age); // 存在しないプロパティは undefined
        
undefinedは「まだ設定されていない」状態を表す。

null が現れる場面


// APIレスポンス
interface UserProfile {
  name: string;
  avatar: string | null; // 意図的にnullを許可
}

// データベースクエリ
const findUser = (id: number): User | null => {
  // ユーザーが見つからない場合はnullを返す
  return users.find(u => u.id === id) || null;
};

// 初期化時の明示的な空状態
let selectedStand: Stand | null = null;
        
nullは「意図的に空である」ことを明示する。

strictNullChecks モード


// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true
  }
}
        
  • null/undefinedを型に明示的に含めないとエラー
  • 実行時エラーを防ぐために必須
  • 既存プロジェクトでは段階的に有効化

strictNullChecks の効果

  • strictNullChecks: false
    
    let standName: string = null; // OK(危険)
    
  • strictNullChecks: true
    
    let standName: string = null; // ✕ エラー
    
    // 正しい書き方
    let standName: string | null = null; // ✓ OK
    let standUser: string | undefined; // ✓ OK
            
  • 型安全性が格段に向上する

    Optional Chaining【?.】

    安全なプロパティアクセス

    
    interface Stand {
      name: string;
      user?: {
        name: string;
        age?: number;
      };
    }
    
    const theWorld: Stand = { name: "The World" };
    
    
    // 従来の書き方
    const userName = theWorld.user && theWorld.user.name;
    const userAge = theWorld.user && theWorld.user.age; 
    
    
    // Optional chaining
    const userName = theWorld.user?.name; 
    const userAge = theWorld.user?.age;   
    
    チェーンの途中でnull/undefinedに遭遇したら即座にundefinedを返す。

    Nullish Coalescing Operator【??】

    null/undefinedのときのみデフォルト値を設定

    
    // 従来の方法(問題あり)
    const displayName = user.name || "dio"; // 空文字でもdioになる
    
    // Nullish coalescing(正確)
    const displayName2 = user.name ?? "dio"; // null/undefinedのときのみdio
    
    // 実用例
    const config = {
      timeout: userSettings.timeout ?? 5000,
      retries: userSettings.retries ?? 3,
      endpoint: userSettings.endpoint ?? "https://api.example.com"
    };
            
    ||演算子と違って、0や空文字などのfalsyな値は通す。

    型ガードによる安全なnullチェック

    
    function processStand(stand: Stand | null) {
      if (stand === null) { // 型ガード
        console.log("standが見つかりません");
        return;
      }
      
      // この時点でstandはStand型として扱われる
      console.log(`stand名: ${stand.name}`);
      
      // ネストした型ガード
      if (stand.user) {
        console.log(`user名: ${stand.user.name}`);
      }
    }
            
    TypeScriptの型システムがnullチェック後の型を自動的に絞り込む。

    Non-null Assertion Operator【!】

    「絶対null/undefinedじゃない」と断言

    
    function getStandPower(stand?: Stand) {
      // 危険:standがundefinedの可能性
      return stand!.name.toUpperCase();
    }
    
    // より安全な書き方
    function getStandPowerSafe(stand?: Stand) {
      if (!stand) {
        throw new Error("standが必要です");
      }
      return stand.name.toUpperCase();
    }
            
    危険。nullチェックを省略する代わりに、自分で責任を持つ必要あり。

    まとめ

    • null:意図的な空値、undefined:未初期化
    • strictNullChecksは必須設定
    • ?.で安全なアクセス、??でデフォルト値
    • 型ガードで適切なnullチェック
    • !の使用は可能な限り避けること
    null/undefinedは多くのバグの原因。型システムを活用して安全に扱おう。

    Type Assertions【型アサーション】

    「この値は絶対この型だ」とTypeScriptに伝える

    
    // 2つの書き方
    const userInput = document.getElementById("name") as HTMLInputElement;
    const userInput2 = <HTMLInputElement>document.getElementById("name");
    
    // APIレスポンスの型アサーション
    const apiResponse = await fetch("/api/user");
    const userData = await apiResponse.json() as User;
            
    コンパイラが型を推論できない場合に使用。JSXでは<>構文は使えない。

    Type Assertions vs 型ガード

    危険:型アサーションは実行時チェックなし
    
    const maybeUser = getData() as User;
    console.log(maybeUser.name); // 実行時エラーの可能性
    

    安全:型ガードは実行時チェックあり
    
    function isUser(obj: any): obj is User {
      return obj && typeof obj.name === "string";
    }
    
    const data = getData();
    if (isUser(data)) {
      console.log(data.name); // 安全
    }
            
    型アサーションは「信頼」、型ガードは「検証」

    const assertions

    値を変更不可能な定数として扱う

    
    // 通常の配列
    const stands = ["za warudo", "killer queen"]; // string[]
    
    // const assertion
    const standsConst = ["za warudo", "killer queen"] as const;
    // readonly ["za warudo", "killer queen"]
    
    // オブジェクトの場合
    const config = {
      timeout: 5000,
      retries: 3
    } as const;
    // { readonly timeout: 5000; readonly retries: 3 }
            
    型を正確に推論させたい場合に有効。特にunion型の作成時に便利。

    Type Assertions 使用時の注意点

    • 実行時の型チェックは行われない
    • 「型の嘘」を可能にしてしまう
    • unknown → 具体的な型への変換で使用
    • デバッグが困難になる可能性
    
    // 危険な例
    const badCast = "jotaro" as any as number;
    console.log(badCast * 2); // 実行時エラー
    
    // 適切な例
    const jsonData = JSON.parse(response) as User;
            
    型アサーションは最後の手段。できるだけ型ガードや適切な型定義を使おう。

    理解度チェック⑥

    以下のコードの出力は?

    const user = {
      score: 0,
      avatar: "",  // 空の文字列 - ユーザーがアバターを削除した
      backup: null as string | null // 最初はnull、あとで文字列(URLなど)を入れるための型指定
    };
    
    const result1 = user.score || 100;
    const result2 = user.score ?? 100;
    const result3 = user.avatar || "default.jpg";
    const result4 = user.avatar ?? "default.jpg";
    const result5 = user.backup || "fallback.jpg";
    const result6 = user.backup ?? "fallback.jpg";
    console.log(result1, result2, result3, result4, result5, result6);
    
    A) 100, 100, "default.jpg", "default.jpg", "fallback.jpg", "fallback.jpg"
    B) 100, 0, "default.jpg", "", "fallback.jpg", "fallback.jpg"
    C) 0, 0, "", "", null, null
    D) 100, 0, "default.jpg", "", null, null
    答え: B) || は falsy値 (0, "") で fallback するが、?? は null/undefined のみ。
    avatarが空文字の場合: || は "default.jpg"、?? は "" を保持

    クラス & アクセス修飾子

    オブジェクト指向の基本構造

    プロトタイプ継承とは?

    JavaScriptのオブジェクト指向の基本メカニズム

    • オブジェクトが他のオブジェクトから直接継承する
    • クラスベースの言語とは異なるアプローチ
    • 全てのオブジェクトはprototypeチェーンを持つ

    プロトタイプチェーン

    
    const animal = {
      speak: function() {
        return "何か音を出す";
      }
    };
    
    const dog = Object.create(animal);
    dog.bark = function() {
      return "わんわん!";
    };
    
    console.log(dog.speak()); // "何か音を出す" ← animalから継承
    console.log(dog.bark());  // "わんわん!" ← 自分のメソッド
      

    他の言語との比較

    PHP/Ruby (クラスベース)

    
    class Animal {
      public function speak() {
        return "音を出す";
      }
    }
    
    class Dog extends Animal {
      public function bark() {
        return "わんわん";
      }
    }
          

    設計図(クラス) → インスタンス

    JavaScript (プロトタイプベース)

    
    function Animal() {}
    Animal.prototype.speak = function() {
      return "音を出す";
    };
    
    
    function Dog() {}
    Dog.prototype = Object.create(Animal.prototype);
    Dog.prototype.bark = function() {
      return "わんわん";
    };
          

    オブジェクト → オブジェクト

    JSのclassは糖衣構文

    実際には同じ

    
    class Dog {
      constructor(name) {
        this.name = name;
      }
      
      bark() {
        return "わんわん";
      }
    }
    
    
    function Dog(name) {
      this.name = name;
    }
    
    Dog.prototype.bark = function() {
      return "わんわん";
    };
        

    なぜ理解が重要?

    • thisの動作を理解するため
    • 継承の仕組みを把握するため
    • デバッグ時に内部動作を追跡できる
    • TypeScriptの型システムとの関係
    classはプロトタイプの「見た目をよくしたもの」

    基本的なクラス定義

    
    class Stand {
        name: string;         // クラスプロパティ
    
        constructor(name: string, public power: number) { // public引数はプロパティの自動宣言+初期化
            this.name = name;   // コンストラクタでプロパティを初期化
        }
    
        attack(): string {     // クラスメソッド
            return `${this.name}で攻撃!パワー: ${this.power}`;
        }
    }
    
    
    const starPlatinum = new Stand("スタープラチナ", 100);
    console.log(starPlatinum.attack()); // "スタープラチナで攻撃!パワー: 100"
    console.log(starPlatinum.power);    // 100 (public引数で自動生成)
        
    constructor(public prop: type)this.prop = prop + プロパティ宣言の省略形

    アクセス修飾子

    
    class Character {
        public name: string;        // どこからでもアクセス可能
        private hp: number;         // クラス内のみ
        protected level: number;    // 継承先でも使用可能
        
        constructor(name: string, hp: number, level: number) {
            this.name = name;
            this.hp = hp;
            this.level = level;
        }
        
        private heal(): void {
            this.hp += 20;
        }
    }
        
    デフォルトはpublicprivateは外部からアクセス不可、protectedは継承先でのみ使用可能。

    継承とオーバーライド

    
    class Villain extends Character {
        private evilPlan: string;
        
        constructor(name: string, hp: number, level: number, plan: string) {
            super(name, hp, level); // 親クラスのコンストラクタを呼び出し
            this.evilPlan = plan;
        }
        
        public getStatus(): string { // メソッドオーバーライド
            return `${super.getStatus()}, 計画: ${this.evilPlan}`;
        }
        
        protected useSpecialAbility(): void {
            console.log(`${this.name}が特殊能力を使用!レベル${this.level}の力で!`);
        }
    }
    
    const dio = new Villain("ディオ", 200, 50, "世界征服");
    console.log(dio.getStatus()); // "ディオ: HP 200, Lv.50, 計画: 世界征服"
    // dio.useSpecialAbility();   // ✕ エラー: protected
        
    super(): 親のコンストラクタ/メソッド呼び出し、protectedは継承先でのみ使用可能

    readonly & static

    
    class ClubMember {
        readonly membershipId: string;
        private static memberCount: number = 0;
        static readonly clubName: string = "雛見沢分校";
        
        constructor(public name: string, membershipId: string) {
            this.membershipId = membershipId;
            ClubMember.memberCount++; // private staticもクラス内からアクセス可能
        }
        
        static getClubInfo(): string {
            return `${ClubMember.clubName}: ${ClubMember.memberCount}人`; // staticプロパティはクラス名でアクセス
        }
    }
    
    
    const keiichi = new ClubMember("前原圭一", "001");
    const rena = new ClubMember("竜宮レナ", "002");
    console.log(ClubMember.getClubInfo());  // "雛見沢分校: 2人"
    // keiichi.membershipId = "003";        // ✕ エラー: readonly
    // console.log(ClubMember.memberCount); // ✕ エラー: private static
        
    readonly: 初期化後変更不可、static: クラス自体に属する(インスタンス不要)、
    private static: クラス内でのみアクセス可能

    getter/setter

    
    class Pokemon {
        private _hp: number;
        
        constructor(public name: string, private _maxHp: number) {
            this._hp = _maxHp;
        }
        
        get hp(): number { return this._hp; }
        
        set hp(value: number) {
            this._hp = Math.max(0, Math.min(value, this._maxHp));
        }
        
        get healthPercentage(): number {
            return Math.round((this._hp / this._maxHp) * 100);
        }
    }
        
    
    const pikachu = new Pokemon("ピカチュウ", 100);
    pikachu.hp = 150;        // setter (maxHpに制限される)
    console.log(pikachu.hp); // 100
    console.log(pikachu.healthPercentage); // 100 (read-only)
        
    get: プロパティアクセス時の処理、set: 代入時の検証・変換

    抽象クラス

    
    abstract class Weapon {
        constructor(public name: string, public damage: number) {}
        
        abstract attack(): string; // 子クラスで実装必須
        
        public getInfo(): string { // 共通の実装
            return `${this.name} (威力: ${this.damage})`;
        }
    }
    
    class Sword extends Weapon {
        attack(): string {
            return `${this.name}で斬りつけた!${this.damage}のダメージ!`;
        }
    }
        
    
    // const weapon = new Weapon("", 0); // ✕ エラー: 抽象クラスはインスタンス化不可
    const sword = new Sword("エクスカリバー", 50);
    console.log(sword.attack()); // "エクスカリバーで斬りつけた!50のダメージ!"
        
    abstract: インスタンス化不可、子クラスでの実装強制。共通処理と個別実装を分離

    まとめ

    • クラス: オブジェクト指向の基本構造
    • アクセス修飾子: public, private, protected
    • 継承: extends, super, オーバーライド
    • getter/setter: プロパティアクセスの制御
    • static: インスタンス不要のクラス機能
    • abstract: 継承強制の抽象クラス
    TypeScriptのclassは本質的にJavaScriptのプロトタイプベース継承の糖衣構文

    モジュール

    コードの整理と再利用

    エクスポート

    export でモジュール外部に公開。

    
    // stands.ts
    export class Stand {
        constructor(public name: string, public power: number) {}
    }
    
    export const STAND_TYPES = {
        CLOSE_RANGE,
        LONG_RANGE
    } as const;
    
    // デフォルトエクスポート
    export default class StandUser {
        constructor(public name: string, public stand: Stand) {}
    }
        
    default export は1つまで、named export は複数可能

    インポート

    import でモジュールを読み込み。

    
    // main.ts
    import StandUser, { Stand, STAND_TYPES } from './stands';
    
    import { Stand as StandClass } from './stands';
    
    import * as StandModule from './stands';
    
    const jotaro = new StandUser("承太郎", new Stand("スタープラチナ", 100));
    const dio = new StandModule.default("DIO", new StandModule.Stand("ザ・ワールド", 95));
        
    * as で全てを名前空間オブジェクトとして取得。
    default exportModuleName.default でアクセス

    まとめ

    • export/import: ファイル間でコード共有
    • default export: 1つまで、named export: 複数可
    • import { } from: 必要なものだけ取得
    • import * as: 全部まとめて名前空間で

    ジェネリクス

    ジェネリクスとは

    型を「後で決める」仕組み

    • 関数やクラスを書く時点では型を決めない
    • 使う時に具体的な型を指定
    • 型安全性を保ちながら再利用可能なコードが書ける
    ジェネリクスは「汎用的な」という意味。一つのコードで複数の型に対応できる仕組み。

    なぜ必要?

    // ジェネリクスなしだと...
    function getFirstStand(stands: Stand[]): Stand { return stands[0]; }
    function getFirstFragment(fragments: Fragment[]): Fragment { return fragments[0]; }
    // 型ごとに同じ関数を作る必要がある
    
    // ジェネリクスがあると
    function getFirst<T>(items: T[]): T {
      return items[0];
    }
    
    // 一つの関数で全てに対応
    const firstStand = getFirst(stands); // Stand型
    const firstFragment = getFirst(fragments); // Fragment型
    同じ処理でも型が違うと別々の関数が必要になる。ジェネリクスで一つの関数で済む。

    基本的な使い方

    // 関数のジェネリクス
    function identity<T>(arg: T): T {
      return arg;
    }
    
    // 使い方
    const result1 = identity<string>("za warudo"); // 明示的に型指定
    const result2 = identity("muda muda"); // 型推論で自動判定
    
    // 型が保持される
    console.log(result1.length); // OK: string のメソッドが使える
    console.log(result2.toUpperCase()); // OK: 型推論で string と判定
    <T> が型パラメータ。関数呼び出し時に具体的な型が決まる。
    型推論があるので明示的に書かなくてもOK。

    実はもう使っている!

    // 普段書いているこれもジェネリクス
    const stands: string[] = ["star platinum", "the world"];
    const numbers: number[] = [1, 2, 3];
    
    // 実は以下と同じ意味
    const stands: Array<string> = ["star platinum", "the world"];
    const numbers: Array<number> = [1, 2, 3];
    
    // Promise も同じ
    const response: Promise<string> = fetch("/api").then(r => r.text());
    Array<T>, Promise<T> などはジェネリクスの典型例。
    <T> の部分に具体的な型を入れて使う。もう慣れ親しんでいる!

    基本的な使い方

    // 複数の型パラメータ - 関連する異なる型を扱う時
    function createPair<T, U>(first: T, second: U): [T, U] {
      return [first, second];
    }
    
    // ユーザーとそのメインスタンドをペアにする
    const jotaro = createPair("jotaro", { name: "star platinum", power: 95 });
    // [string, Stand]
    
    // APIレスポンスとメタデータをペアにする  
    const response = createPair(userData, { timestamp: Date.now() });
    // [User, { timestamp: number }]
    複数の型パラメータは関連する異なる型を組み合わせる時に使う。
    <T, U> は慣例的な名前。<TKey, TValue><TData, TMeta> など意味のある名前でもOK。

    理解度チェック⑦

    なぜこのコードは any よりジェネリクスの方が良い?

    // any 版
    function processData(data: any): any {
      return data;
    }
    
    // generics 版
    function processData<T>(data: T): T {
      return data;
    }
    
    const result = processData("jotaro");
    
    A) 実行速度が速い
    B) 型安全性と補完機能を保持
    C) メモリ使用量が少ない
    D) 書くコードが短い
    答え: B) any は型情報を失うが、generics は型を保持。型エラーも検出できる。

    配列操作の例

    
    // ジェネリクスを使った汎用的な関数
    function findByName<T extends { name: string }>(items: T[], name: string): T | undefined {
      return items.find(item => item.name === name);
    }
    
    const stands: Stand[] = [
      { name: "star platinum", user: "jotaro", power: 95 },
      { name: "the world", user: "dio", power: 90 }
    ];
    
    const weapons: Weapon[] = [
      { name: "arrow", damage: 50, type: "ranged" },
      { name: "requiem arrow", damage: 100, type: "ranged" }
    ];
    
    
    // 型安全に使える
    const starPlatinum = findByName(stands, "star platinum"); // Stand | undefined
    const arrow = findByName(weapons, "arrow"); // Weapon | undefined
    extendsで型に制約を加えることができる。nameプロパティを持つ型のみ受け入れる。

    実用的な例:APIレスポンス

    interface ApiResponse<T> {
      data: T;
      status: "success" | "error";
    }
    
    // 汎用的なAPI関数
    async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
      const response = await fetch(url);
      return response.json();
    }
    // 使用時に型が決まる
    const standsResponse = await fetchData<Stand[]>("/api/stands");
    const userResponse = await fetchData<User>("/api/user");
    
    // 型安全にアクセス
    if (standsResponse.status === "success") {
      standsResponse.data.forEach(stand => {
        console.log(stand.name); // Stand型として扱われる
      });
    }
    APIレスポンスの共通構造をジェネリクスで定義。dataの型だけ変えて再利用できる。

    理解度チェック⑧

    この関数の型パラメータ T の制約は何?

    function findByPower<T extends { power: number }>(
      items: T[], 
      minPower: number
    ): T[] {
      return items.filter(item => item.power >= minPower);
    }
    
    A) T は任意の型
    B) T は power プロパティ(number型)を持つ型
    C) T は配列型のみ
    D) T は number 型のみ
    答え: B) extends { power: number } は「power プロパティを持つ型」の制約。
    power を持つ任意のオブジェクト型に対応できる。

    理解度チェック⑨

    以下のコードの出力は?

    function echo<T>(value: T): T {
      return value;
    }
    
    const result1 = echo("za warudo");
    const result2 = echo<string>("muda muda");
    const result3 = echo(42);
    
    console.log(typeof result1, typeof result2, typeof result3);
    
    A) object, object, object
    B) string, string, number
    C) any, string, any
    D) T, T, T
    答え: B) 型推論により result1 は string、明示指定で result2 も string、result3 は number。
    generics は実行時には消えるが、開発時の型チェックで正しい型を保持する。

    ジェネリクスの利点

    • 再利用性: 一つのコードで複数の型に対応
    • 型安全性: anyを使わずに型チェックを維持
    • 補完機能: エディタの型情報に基づく支援
    • 保守性: 型変更時の影響範囲が明確

    使うべき場面

    • 配列操作、データ変換の汎用関数
    • APIレスポンスの型定義
    • 複数の型で同じ処理をしたい時
    • ライブラリやユーティリティ関数の作成
    型の「形」が同じで処理も同じなら、ジェネリクスを検討する。型安全性を保ちながらコードの重複を避けられる。

    Index Signatures

    動的なオブジェクト構造を扱う方法

    Index Signatures【インデックスシグネチャ】

    // プロパティ名が事前に分からない場合
    interface StringDictionary {
        [key: string]: string;
    }
    
    interface NumberDictionary {
        [key: string]: number;
        length: number; // ok, lengthはnumber型
        // name: string; // error, インデックスシグネチャと競合
    }
    // 複数のインデックスシグネチャ
    interface MixedDictionary {
        [key: string]: any;
        [key: number]: string; // 数値キーは文字列キーに割り当て可能である必要がある
    }
    オブジェクトのプロパティ名が動的に決まる場合に使用する。APIレスポンスや設定オブジェクトでよく使われる。

    まとめ

    存在する理由

    • 動的なオブジェクト構造
    • APIレスポンスの型定義
    • ライブラリの内部実装

    なぜ避けるべきか

    • 型安全性の喪失
    • IDE支援の低下
    • バグの発見が困難
    
    // ✕ 非推奨: 型安全なし
    interface Config {
        [key: string]: any;
    }
    
    
    // ✓ 推奨: 型明示で安全
    interface Config {
        apiUrl: string;
        timeout: number;
        retries?: number;
    }
    

    現実的な判断: 必要な時もあるが、まず具体的な型定義を検討する

    Conditional Types【条件付き型】

    型レベルでの条件分岐

    基本的なConditional Types

    // T extends U ? X : Y の形
    // 「TがUを継承/拡張できるか?」で分岐
    type IsString<T> = T extends string ? true : false;
    
    type Test1 = IsString<string>;  // true
    type Test2 = IsString<number>;  // false
    type Test3 = IsString<"hello">;  // true(文字列リテラル型はstring型に含まれる)
    extends は「継承」ではなく「含まれる/代入可能」の意味。
    T extends string は「Tがstring型に代入可能か?」を判定。

    実用例

    // null/undefinedを除外する型
    type NonNull<T> = T extends null | undefined ? never : T;
    
    type SafeString = NonNull<string | null>;  // string
    type SafeNumber = NonNull<number | undefined>;  // number
    
    // 配列かどうかを判定
    type IsArray<T> = T extends unknown[] ? true : false;
    
    type ArrayCheck1 = IsArray<string[]>;  // true
    type ArrayCheck2 = IsArray<string>;    // false
    conditional typesは型の「フィルタリング」に使われることが多い。
    TypeScriptの組み込みユーティリティ型の多くがこれで実装されている。

    Utility Types【ユーティリティ型】

    TypeScriptが提供する便利な型変換ツール

    基本的なユーティリティ型

    interface User {
        id: number;
        name: string;
        email: string;
        isActive: boolean;
    }
    // Partial<T> - 全プロパティを省略可能に
    type UserUpdate = Partial<User>; // { id?: number; name?: string; email?: string; isActive?: boolean; }
    
    // Pick<T, K> - 特定のプロパティだけ選択
    type UserProfile = Pick<User, "name" | "email">; // { name: string; email: string; }
    
    // Omit<T, K> - 特定のプロパティを除外
    type UserWithoutId = Omit<User, "id">; // { name: string; email: string; isActive: boolean; }
    
    // Record<K, T> - キーと値の型を指定してオブジェクト型作成
    type UserStatus = Record<string, boolean>; // { [key: string]: boolean; }
    既存の型から新しい型を作成する際に使用する。
    手動で型を定義し直す必要がなく、元の型が変更されても自動で追従する。

    実用例

    // API更新用(一部のフィールドのみ)
    function updateUser(id: number, updates: Partial<User>) {
        // name だけ更新、email だけ更新などが可能
    }
    // 公開用プロフィール(idを隠す)
    function getPublicProfile(user: User): Omit<User, "id"> {
        const { id, ...publicData } = user;
        return publicData;
    }
    // 設定値の管理
    const config: Record<string, string> = {
        apiUrl: "https://api.example.com",
        theme: "dark"
    };
    既存の型から新しい型を作るときに重宝する。
    特にAPI設計やフォーム処理でよく使う。

    ユーティリティ型の使い分け

    • Partial<T>: 更新処理で一部のフィールドのみ受け取りたい
    • Pick<T, K>: 大きな型から必要な部分だけ抽出したい
    • Omit<T, K>: 特定のフィールドを隠したい(公開API等)
    • Record<K, T>: 動的なキーを持つオブジェクト型を作りたい
    元の型定義が変更されても自動で追従するため、保守性が高い。
    手動で型を複製するより、ユーティリティ型を使う方が安全。

    TypeScript 実践演習

    2つの課題 (各5-15分)

    今日学んだ内容を実際に使ってみよう!

    課題1 ユーティリティ型でユーザー型変換

    以下の User から新しい型を作成してください

    interface User {
        id: number;
        name: string;
        email: string;
        password: string;
        isActive: boolean;
    }
    • PublicUser: password を除いた型
    • UserUpdate: id を除き、全て省略可能な型
    • UserSummary: idname のみの型

    課題1 解答例

    ユーティリティ型を組み合わせて必要な型を作成

    // password を除いた型
    type PublicUser = Omit<User, "password">;
    
    // id を除き、全て省略可能
    type UserUpdate = Partial<Omit<User, "id">>;
    
    // 指定されたプロパティのみ
    type UserSummary = Pick<User, "id" | "name">;

    課題2 ジェネリクスでデータ変換

    以下の関数をジェネリクスを使って型安全に書き直してください

    // 現在の実装(型が不十分)
    function transformArray(items: any[], transformer: (item: any) => any): any[] {
        return items.map(transformer);
    }
    
    // 使用例
    const numbers = [1, 2, 3];
    const strings = transformArray(numbers, (n) => n.toString());
    const lengths = transformArray(["hello", "world"], (s) => s.length);
    • 入力の配列の型を保持する
    • 変換関数の入力・出力型を正しく推論する
    • 戻り値の型を正確に推論する
    ヒント: 2つの型パラメータが必要です <T, U>

    課題2 解答例

    ジェネリクスで入力と出力の型を関連付ける

    function transformArray<T, U>(items: T[], transformer: (item: T) => U): U[] {
        return items.map(transformer);
    }
    
    // 使用例 - 型推論が正しく動作
    const numbers = [1, 2, 3];
    const strings = transformArray(numbers, (n) => n.toString()); // string[]
    const lengths = transformArray(["hello", "world"], (s) => s.length); // number[]
    
    // TypeScriptが型エラーを検出
    // transformArray(numbers, (n) => n.charAt(0)); // Error: charAt は number にない
    2つの型パラメータ <T, U> を使って入力と出力の型を関連付けている

    第2回まとめ

    • 型システム:型エイリアス、インターフェース、列挙型
    • 型アサーション:型アサーションとconstアサーション
    • null/undefinedの扱い:安全チェック
    • クラス:アクセス修飾子とプロトタイプ継承の基礎
    • モジュール:基本的なimport/export
    • ジェネリクス:再利用可能な型の作成
    • インデックスシグネチャ:動的なキーの型付け
    • 条件型:基本的な条件型の使用例
    • ユーティリティ型:Partial、Pick、Omit、Recordなどの活用

    宿題・実践課題

    斬魄刀管理システム改良版

    次回予告

    React基礎①: Reactとは、JSX、コンポーネント、Propsの使い方