0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScript覚え書き #1

0
Last updated at Posted at 2026-10-06

表現の改善にAIを活用しています。
あらかじめご了承ください。

extends

TypeScriptの extends は4つの使い方があります。

1.クラスの継承

JavaScriptと同じく、親クラスのプロパティやメソッドを引き継ぎます。継承できる親クラスは1つだけです。

class Animal {
  constructor(public name: string) {}
  move() {
    console.log(`${this.name} が移動した`);
  }
}

class Dog extends Animal {
  bark() {
    console.log("ワン!");
  }
}

const d = new Dog("ポチ");
d.move(); // 親のメソッドが使える
d.bark();

2.インターフェースの拡張

既存のインターフェースに項目を追加した新しい型を作ります。クラスと違い、複数を同時に拡張できます。

interface Named {
  name: string;
}

interface Aged {
  age: number;
}

interface PersonI extends Named, Aged {
  email: string;
}
// PersonI は name, age, email を持つ

3.ジェネリクスの制約

型パラメータに「この条件を満たす型だけ受け付ける」という制限をかけます。ここでの A extends B は「継承」ではなく、「A が B に代入可能である」 という意味です。

function getLength<T extends { length: number }>(x: T): number {
  return x.length; // T は length を持つことが保証されているので OK
}

getLength("hello");   // OK
getLength([1, 2, 3]); // OK
getLength(123);       // エラー:number には length がない

「代入可能」とは

「A が B に代入可能」とは、B 型の変数に A 型の値を入れてもエラーにならないことです。実際にコードで確かめてみましょう。

let withLength: { length: number };

withLength = "hello";   // OK
withLength = [1, 2, 3]; // OK
withLength = 123;       // エラー

T extends { length: number } は、この代入が通る型だけを T として受け付けるという条件です。

なぜ string や配列は代入できるのか

型 { length: number } が要求しているのは、「number 型の length プロパティを持っていること」だけです。ほかにどんなプロパティを持っていても構いません。

ではstring 型、配列型、number 型はそれぞれどんなメンバーを持っているのでしょうか。一部を抜き出し書きます。

// string 型のメンバー(抜粋)
{
  length: number;            // ← ある!
  charAt(pos: number): string;
  toUpperCase(): string;
  slice(start?: number, end?: number): string;
  // ...
}

// number[] 型のメンバー(抜粋)
{
  length: number;            // ← ある!
  push(...items: number[]): number;
  map<U>(callbackfn: (value: number) => U): U[];
  // ...
}

// number 型のメンバー(抜粋)
{
  toFixed(fractionDigits?: number): string;
  toString(radix?: number): string;
  // ...                     // ← length がない!
}

string 型と配列型は条件を満たしているので代入できます。一方、numberは length を持っていないので代入できません。

string や number はプリミティブ型ですが、プロパティを参照するとき TypeScript はそれぞれ String インターフェース、Number インターフェース(どちらも lib.es5.d.ts)のメンバーとして扱います。上の「string 型のメンバー」「number 型のメンバー」は、これらのインターフェースから抜粋したものです。同様に、配列型 number[] のメンバーは Array<number> インターフェースのものです。

構造的型付け

このようにTypeScriptは「どの型として作られたか」という出自ではなく、「必要なものを持っているか」という中身で判断します。この考え方を構造的型付け(Structural Typing) と呼びます。

そのため、{ length: number } を継承していない自作のクラスでも、length を持っていれば条件を満たします。

class Rope {
  length = 50; // どこからも継承していない
}

getLength(new Rope()); // OK!

4. 条件型(Conditional Types)

型レベルの三項演算子です。T extends U ? X : Y は「TはUに代入可能ならX、そうでなければY」という意味になります。

type IsString<T> = T extends string ? true : false;

type A = IsString<"abc">; // true
type B = IsString<42>;    // false

typeとinterfaceの比較

1.表現できる型の範囲

type はあらゆる型に名前を付けられます。一方 interface は、メンバーを 1 つずつ書き並べて定義する構文です。そのため、表せるのはメンバーの宣言を書き並べられるオブジェクト型だけで、それも一定の条件を満たすものに限られます(詳しくは後述します)。

つまり、interface で書ける型はすべて type でも書けます。

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

// typeで書ける
// type Person = { name: string, age: number };

一方、type で書ける型のうち、interface でも書けるのは一部だけです。

// interface でも書ける型(オブジェクト型)
type Point = { x: number; y: number };
// interface Point { x: number; y: number }

// interface では書けない型(オブジェクト型以外)
type Id = string;                     // プリミティブ型の別名
type Status = "active" | "inactive";  // ユニオン型
type ElementType<T> = T extends (infer U)[] ? U : never; // Conditional Types

interfaceのメンバーとキー

  • メンバー:interface の { } に書く宣言 1 つ 1 つのこと。
  • キー:プロパティやメソッドの名前の部分(name: string の name)。コールシグネチャやコンストラクトシグネチャには、そもそもキーがありません。

メンバー制約

interface で書けるのは、「メンバーがすべて静的に確定しているオブジェクト型」 です。interface のメンバーになれるのは、次の 7 種類だけです。

interface Example {
  name: string;                        // プロパティシグネチャ
  greet(msg: string): void;            // メソッドシグネチャ
  (x: number): string;                 // コールシグネチャ
  new (x: number): Example;            // コンストラクトシグネチャ
  [key: string]: unknown;              // インデックスシグネチャ
  get id(): string;                    // アクセサ宣言(TS 4.3+)
  set id(v: string);
}

下記の型はオブジェクト型ですが、静的に確定しているオブジェクト型に該当しないため、interface で書けません。

// キーが型パラメータに依存して決まるオブジェクト型
type MyPartial<T> = { [K in keyof T]?: T[K] };
type Dict<K extends string, V> = { [P in K]: V };

type Pair = [string, number]; のようなタプル型は特殊であるため、ここでは詳しく論じません。結論としては、タプル型も interface で書けません。

ただし、型パラメータを含む type がすべて interface で書けないわけではありません。型パラメータがメンバーの型に使われているだけなら、どのメンバーがあるかは確定しているので、interface でも書けます。

// type で書いた型
type Box<T> = { value: T };
type Pair<A, B> = { first: A; second: B };
type Dict<T> = { [key: string]: T };

// 同じ型を interface で書いたもの
interface BoxI<T> { value: T }
interface PairI<A, B> { first: A; second: B }
interface DictI<T> { [key: string]: T }

また Mapped Types 構文が interface のメンバーになれません。

interface Flags {
  [K in "a" | "b"]: boolean; // エラー
}

interface MyPartialI<T> {
  [K in keyof T]?: T[K]; // エラー
}

キー制約

プロパティ名制約

プロパティ名は識別子・文字列リテラル・数値リテラル、またはリテラル型か unique symbol 型を持つ式の計算プロパティ名でなければなりません。

// 計算プロパティ名で使う定数
const strKey = "email";            // 型は "email"(文字列リテラル型)
const numKey = 404;                // 型は 404(数値リテラル型)
const idSym: unique symbol = Symbol("id"); // 型は unique symbol

interface Sample {
  // 1. 識別子
  name: string;

  // 2. 文字列リテラル(識別子として書けない名前も使える)
  "content-type": string;

  // 3. 数値リテラル
  200: string;

  // 4. 計算プロパティ名(リテラル型を持つ式)
  [strKey]: string;   // 文字列リテラル型 → "email" というキーになる
  [numKey]: string;   // 数値リテラル型 → 404 というキーになる

  // 5. 計算プロパティ名(unique symbol 型を持つ式)
  [idSym]: number;
}

const sample: Sample = {
  name: "Alice",
  "content-type": "application/json",
  200: "OK",
  email: "alice@example.com",
  404: "Not Found",
  [idSym]: 1,
};

下記のコードがエラーになります。

// ---- エラーになる例 ----

let wideKey = "email";         // let なので型は string(リテラル型ではない)
const plainSym: symbol = Symbol(); // 型注釈で symbol に広げている(unique symbol ではない)

interface Invalid {
  [wideKey]: string;   // エラー: string 型の式は使えない
  [plainSym]: number;  // エラー: symbol 型の式は使えない
}
メソッド名の制約

メソッド名の制約は、プロパティ名とほぼ同じです。違うのは、同じ名前のメソッドを複数書くとオーバーロードになりますが、同じ名前のプロパティを複数書いたり、プロパティとメソッドを両方書いたりすると重複エラーになります。

interface Overloads {
  parse(input: string): object;
  parse(input: number): object;   // OK: オーバーロード
}

interface Conflict1 {
  value: string;
  value: string;                // エラー: 識別子の重複
}

interface Conflict2 {
  value: string;
  value(): string;                // エラー: 識別子の重複
}

2.拡張関連

拡張の仕方

type の拡張は交差型(&)、interface の拡張はインターフェース拡張(extends)で実現します。

仕方が違っても、下記のコードで定義された Person と PersonI が実質的に同じ型として扱われます。

type Name = { name: string };
type Age = { age: number };
type Person = Name & Age & { gender: 'male' | 'female'  };

interface Named {
    name: string
};
interface Aged {
    age: number
};
interface PersonI extends Named, Aged {
    gender: 'male' | 'female'
}

実質的に同じだけで、完全に同じというわけではありません。コンパイラ内部の扱いが違います。

  • 交差型 &:構成要素を保持したまま扱われ、プロパティの解決や代入可能性のチェックのたびに各メンバーを再帰的に調べ直す必要があります。
  • interface extends:宣言時に1つのフラットなオブジェクト型としてまとめられます。

衝突時の挙動

インターフェース拡張(extends)はプロパティの型が衝突した時にエラーを教えてくれます。一方、交差型(&)ではエラーにならず、黙って never を生みます。

interface A { value: string }
interface B extends A { value: number } // ❌ 定義時にエラー

type C = { value: string };
type D = C & { value: number };
// エラーなし(value は string & number -> neverになる)
// ただし値を作る時はエラーになる

const d: D = { value: "hello" };
// ❌ Type 'string' is not assignable to type 'never'.

3.宣言マージ

interface は、同じ名前で複数宣言すると自動的に合体します。一方、同じ名前の type を2回宣言するとエラーになります。

interface User { name: string }
interface User { age: number }
// → User は { name: string; age: number }

type Admin = { name: string };
type Admin = { age: number }; // ❌ Duplicate identifier 'Admin'.

interface のこの性質は、ライブラリやグローバルな型を後から拡張する時に使われます。

export {}; // ファイルをモジュールにする(import/export が既にあれば不要)

declare global {
  interface Window {
    myAppConfig: { debug: boolean };
  }
}

window.myAppConfig.debug; // ✅

逆に言えば、意図せず同じ名前のinterfaceを宣言すると、知らないうちに型が合体してしまうリスクもあります。

4.暗黙のインデックスシグネチャ

type で定義したオブジェクト型は Record<string,...> に代入できます。一方、同じ形の interface は代入できません。

type T = { a: string };
interface I { a: string }

const t: T = { a: "x" };
const i: I = { a: "x" };

const r1: Record<string, string> = t; // ✅ OK
const r2: Record<string, string> = i; // ❌ Index signature for type 'string'
                                      //    is missing in type 'I'.

interface は宣言マージで後からプロパティが増える可能性があるため、TypeScript は「すべてのプロパティがstring型である」とは保証できません。そのため、代入を許可しないのです。

typeとinterfaceの使い分け

実務での判断基準

よくある2つの流派

「基本は interface、必要なときだけ type」派

TypeScript公式ハンドブックが推奨しているスタイルです(「type の機能が必要になるまでは interface を使う」)。extends による継承が自然に書けます。

「全部 type で統一」派

Reactコミュニティなどでよくあるスタイルで、書き方が1つに揃い、意図しない宣言マージも起きません。Propsの合成で & を多用する点には注意が必要です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?