TypeScriptにはextendsというキーワードがあるが、文法的に二種類の使い分けがあるのでそれらを学び直す。

レベル感としては初級 - 中級になります。

前提知識

目次

継承に使うextends

JavaやC#のようなオブジェクト指向言語にある機能とほとんど同じだと思う。 その型に備わっているメンバーやプロパティを使えるようにできる。

以下はA ⇒ BならばAから継承したBを表している。

①class ⇒ class

class A {
  foo: string = ''
}

class B extends A {
}

console.log(new B()) // B: { "foo": "" }

②interface ⇒ interface

interface A {
  foo: string
}

interface B extends A {
  bar: number
}

// プロパティfooがないとエラーがでる
const b: B = {
  bar: 0
}

③interface ⇒ class

interfaceで定義したプロパティはクラスに対して継承を用いることができないので、実装を使う。
理由は以下で説明。

interface A {
  foo: string
}

class B implements A {} // プロパティfooを定義しなければエラーがでる

④class ⇒ interface

基本的にはintefaceから継承されるのと意味合いは同じになる。

class A {
  foo: string = ''
}

interface B extends A {}

const b: B = {} // プロパティfooがないとエラーがでる

丁寧に4パターン書いたがそれぞれの挙動を覚える必要はなく、実体であるか概念であるかを考えるとややこしくなくなる。

これらのうち③だけがextendsを使えないが、クラスに何かを継承する場合、インスタンス化時に何らかの値に初期化される必要があるのに対して継承元であるインターフェースは概念でしかなく、実行時には消えてしまうのでクラスに使いたくても「このプロパティも一緒に初期化してくれ」と伝えることぐらいしかできない。
この伝えておくことを実装と呼びキーワードimplementsを使うが、値にはできずとも初期値漏れを未然に防げるので相当有能である。

型定義に使うextends

継承を理解できていれば使い方は異なるものの、意味合いを理解するのは割と難くないはず。
前者のextendsと比べると難易度的は高いが、これは総称型(ジェネリクス)と併用することからそう感じることが多いのだと思う。

またこのextendsはざっくり分けて引数型の制約型の条件分岐の2パターン。

引数型の制約

任意だがある程度想定できる型を引数にしたい場合によく使う。

まずは何もしない関数にジェネリクスで型付け

function returnSame <T>(value: T): T {
    return value
}

const value = returnSame<number>(1)

こういった場合だと引数の型をさほど意識することはないが、実用性を考えると引数にはオブジェクトが入り、関数内でそのプロパティにアクセスすることがしばしば考えられる。
例えば以下だと型Tにプロパティvalueとかないわクソカスがって言われてます。

function returnValue <T>(obj: T): string {
    return obj.value // Property 'value' does not exist on type 'T'.
}

const value = returnValue({ value: '' })

使う時に実装者が100%valueが存在することを確約出来ていてもTSのコンパイラはそんなん知らんわ状態なので事前に教えてやる必要があります。

interface Obj {
  value: string
}

function returnValue <T extends Obj>(obj: T): string {
    return obj.value
}

const value = returnValue({ value: '' })

こういった書き方をすれば型Objを継承している(代入可能である)型Tという宣言になるのでエラーが出なくなります。
入力時、マウス操作時に実行するイベントの共通化を図る途中のソースコードが以下。

function handle <T extends Event>(e: T) {
  if (e.target === null) return
  if (e instanceof KeyboardEvent) {

  } else if (e instanceof MouseEvent) {

  } else if (e instanceof InputEvent) {

  }
}

const k = {} as KeyboardEvent
const m = {} as MouseEvent
const i = {} as InputEvent

handle(m)

キャストあたりは雑だけどKeyboardEventMouseEventInputEventはいずれもEventから派生した型であるためこういった使い方もできる。

型の条件分岐

A extends BBを継承したAとかBはAに代入可能の意味合いがあるがこれと三項演算子をつかって「~ならば〇型を返す」といった書き方ができる。

interface A {
    foo: number
}

type ConditionA<T> = T extends A ? number : string

// aの型はnumberになる
let a: ConditionA<{ bar: number; foo: number }>

オブジェクト型だと直感的で分かりやすいがリテラルの合併型になると少し注意が必要になる。

type B = 'foo' | 'bar'
type ConditionB<T> = T extends B ? number : string

// number型
let b1: ConditionB<'foo'>

// string型
let b2: ConditionB<'hoge'>

// B型の要素も備わっているのでnumber | stringとなる
let b3: ConditionB<'foo' | 'hoge'>
プロフィール画像

ふじわら

よくわからないもので戯れてたら自分のことすらよくわからない人間になってしまいました。

ひっそりYouTubeしてます。