【無料配布】10日間で学べるTypeScript学習資料

【TypeScript】extendsにおけるinterface/ジェネリクス/条件付き型

typescript-extends

本記事では、「TypeScript extends」を軸に、4つのパターンであるinterfaceの継承、classの継承、ジェネリクスの型制約、条件付き型を整理し、実務でどう使い分けるか解説します。

目次

interfaceの拡張(継承)におけるextends

TypeScriptで最も利用されるextendsの役割が、interface(インターフェース)の拡張です。

DRY(Don’t Repeat Yourself)の原則を型定義において実現する基本機能です。

interfaceの拡張(継承)におけるextends
  • 共通の型定義を再利用する基本構文
  • 複数のinterfaceを同時に継承する方法
  • プロパティの上書き(オーバーライド)と注意点

共通の型定義を再利用する基本構文

Webサイトを制作する際、様々なUIコンポーネントを作成すると思います。

例えば、「ボタン」の型があり、アイコン付きの「アイコンボタン」の型を作りたい場合、同じプロパティを何度も書くのは非効率です。

// ベースとなる基本のボタン型
interface BaseButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
}

// BaseButtonPropsを継承(拡張)して、iconプロパティを追加
interface IconButtonProps extends BaseButtonProps {
  icon: string;
}

// IconButtonProps は以下の型と同じになります
// {
//   label: string;
//   onClick: () => void;
//   disabled?: boolean;
//   icon: string;
// }

const myButton: IconButtonProps = {
  label: "送信する",
  onClick: () => console.log("clicked"),
  icon: "send-icon.png" // 追加されたプロパティ
};

A extends Bと書くことで、「Bの構造を持った上で、A独自の構造を付け足す」ことができます。

複数のinterfaceを同時に継承する方法

extendsの強力な点は、カンマ(,)で区切ることで複数のinterfaceを同時に継承できることです。

interface StylingProps {
  className?: string;
  style?: React.CSSProperties;
}

interface LinkProps {
  href: string;
  target?: string;
}

// 複数の型をガッチャンコして新しい型を作る
interface StyledLinkProps extends StylingProps, LinkProps {
  text: string;
}

役割ごとに細かく分割した型定義組み合わせて巨大な型を構築可能になります。

プロパティの上書き(オーバーライド)と注意点

親のinterfaceが持っているプロパティを継承先で「より厳しい型」に上書きすることは可能です。

しかし、「全く関係のない型」に上書きするとエラーになります。

interface Animal {
  name: string;
  age: number | string;
}

interface Dog extends Animal {
  // OK: number | string の範囲内(より厳しい型)への上書きは可能
  age: number; 
  
  // NG: boolean は number | string の範囲外なのでコンパイルエラー!
  // name: boolean; 
}

class(クラス)の継承におけるextends

TypeScript独自の機能ではなく、JavaScript(ES2015/ES6以降)に標準で備わっているオブジェクト指向プログラミングの機能です。

class(クラス)の継承におけるextends
  • オブジェクト指向における親クラスの継承
  • superキーワードを用いたコンストラクタの呼び出し

オブジェクト指向における親クラスの継承

親クラス(スーパークラス)の振る舞い(メソッドやプロパティ)を子クラス(サブクラス)に受け継がせるためextendsを使用します。

class Component {
  elementId: string;

  constructor(id: string) {
    this.elementId = id;
  }

  render() {
    console.log(`${this.elementId} を描画します`);
  }
}

// Componentクラスを継承したCardクラス
class Card extends Component {
  title: string;

  constructor(id: string, title: string) {
    super(id); // 親クラスのコンストラクタを呼び出す必須の処理
    this.title = title;
  }

  renderCard() {
    this.render(); // 親のメソッドを呼び出せる
    console.log(`タイトルは ${this.title} です`);
  }
}

superキーワードを用いたコンストラクタの呼び出し

クラスを継承した場合、子クラスのconstructorの中では、自分自身のthisにアクセスする前に必ずsuper()を呼び出し親クラスの初期化を完了させる必要があります。

これは言語仕様上の厳格なルールです。

ジェネリクスの型制約でのextends

ジェネリクス(<T> のような型の変数)を使用する際extendsは「この型変数 T は、条件を満たさなければならない」といった制約として働きます。

ジェネリクスの型制約でのextends
  • 「何でも入る型」から「条件を満たす型」への制限
  • フロントエンド開発でのコンポーネント設計例

「何でも入る型」から「条件を満たす型」への制限

ジェネリクスは便利ですが、制約をかけないと「どんな型が来るか全くわからない」状態になります。

// Tはどんな型でも良い状態
function getLength<T>(arg: T): number {
  // エラー! Tにはlengthプロパティがあるとは限らない(例:数値が渡された場合)
  // return arg.length; 
}

ここでextendsの出番です。

interface HasLength {
  length: number;
}

// T は「最低限 length: number プロパティを持つ型」に限定される
function getLength<T extends HasLength>(arg: T): number {
  return arg.length; // 今度はエラーにならない!
}

getLength("hello"); // 文字列は length を持つのでOK
getLength([1, 2, 3]); // 配列は length を持つのでOK
// getLength(100); // 数値は length を持たないのでコンパイルエラー!

extendsは、「継承」というよりも「部分型(Subtype)であるか」のチェックとして機能しています。

THasLengthに代入可能な型であること」を強制しています。

フロントエンド開発でのコンポーネント設計例

Web制作の実務において、クライアント向けサイトを構築する際、APIから様々な形のデータを受け取り、リスト表示するコンポーネントを作るとします。

データには必ずidがあることを保証しつつ、それ以外のプロパティは柔軟に受け取りたい場合にextendsが役立ちます。

// 必須プロパティの定義
interface WithId {
  id: string;
}

// Tは必ず id: string を持つオブジェクトでなければならない
function extractIds<T extends WithId>(items: T[]): string[] {
  return items.map(item => item.id);
}

const posts = [{ id: "p1", title: "TypeScript基礎" }, { id: "p2", title: "CSSの極意" }];
const users = [{ id: "u1", name: "田中" }];

// どちらの配列を渡しても、安全に id だけを抽出できる
const postIds = extractIds(posts); 
const userIds = extractIds(users);

ジェネリクスとextendsを組み合わせることで、「高い再利用性」と「堅牢な型チェック」を両立できます。

Conditional Types(条件付き型)におけるextends

TypeScriptの型システムを強力にするのがConditional Types(条件付き型)です。

extendsは、条件分岐(If文)のような役割を果たします。

Conditional Types(条件付き型)におけるextends
  • 型の世界の三項演算子T extends U ? X : Y
  • TypeScriptの組み込みユーティリティ型

型の世界の三項演算子 T extends U ? X : Y

JavaScriptの三項演算子condition ? true : falseと全く同じ構文を「型」に対して適用できます。

// 型Tが string 型に割り当て可能(extends)であれば string 型、そうでなければ number 型になる
type StringOrNumber<T> = T extends string ? string : number;

type Result1 = StringOrNumber<"hello">; // Result1 は string 型になる
type Result2 = StringOrNumber<true>;    // Result2 は number 型になる

extendsは、「左辺の型が、右辺の型に代入可能か(互換性があるか)?」という条件式として機能します。

TypeScriptの組み込みユーティリティ型

TypeScriptには、標準でExcludeExtractといった便利なユーティリティ型が用意されています。

これらも内部ではConditional Typesとextendsを使って定義されます。

例えば、ユニオン型から特定の型を取り除くExcludeの内部実装は以下のようになります。

// TypeScript標準の Exclude の定義
type Exclude<T, U> = T extends U ? never : T;

// 使い方:"a" | "b" | "c" から "a" を取り除く
type MyTypes = Exclude<"a" | "b" | "c", "a">; 
// 結果: "b" | "c"

ユニオン型に対してConditional Typesを適用すると、それぞれの要素に対して個別に条件評価が行われます。

  • "a" extends "a" ? never : "a"never(消滅)
  • "b" extends "a" ? never : "b""b"
  • "c" extends "a" ? never : "c""c"

結果として"b" | "c"だけが残ります。

extendsを使った条件付き型を理解することで、複雑な型を動的に生成する高度な型定義が可能になります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次