👨

TypeScriptのクラスの型と継承を理解する

2022/07/14に公開

こんにちは、早寝早起きが得意です。
Railsバックエンドエンジニアを1年程度をやっており、最近は個人開発のためにフロントエンド周りの勉強をしています。

はじめに

弊社では先日発売された 「プロを目指す人のためのTypeScript入門」 の輪読会が現在開催されています。私は、5.2 クラスの型 5.3 クラスの継承 の輪読当番になり、せっかくなのでzennの記事にまとめることにしました。
本の丸パクリは避けつつも、輪読担当として流れには沿って再まとめ(?)できればと思います。

https://gihyo.jp/book/2022/978-4-297-12747-3

そもそもTypeScriptでクラスっていつ使うの?

私がNext.jsでしかTypeScriptを使ったことがないからなのか、クラスを使う場面が想像できませんでした。
私は普段Railsを触っており、バックエンドの開発ではクラスを利用することで便利になることは想像しやすいです。むしろRailsしか知らないので、クラスを使わないバックエンド開発を想像ができないです。
そのため、バックエンドのTSではクラスが使われるのではないか?と予想していますが、知ってる人がいたら教えてください!

↓らへんの記事を漁っていたら、フロントエンドに関してはクラスはいらなそう?な印象を受けました。

https://qiita.com/u83unlimited/items/834131fba97438323706

https://mizchi.hatenablog.com/entry/2018/07/31/124354

一方で以下の記事ではクラスを使わないと「プロジェクト全体で責務がバラバラでカオスになる」に至る損益分岐点が、かなり早い段階で来ちゃうと思うと書いてありました。
確かに、個人開発でNext.jsを触っている時にHooksとか関数をどこにでも書けるので、気をつけないとカオスになりそうだなと思いました。
https://snamiki1212.com/no-class-application

クラスの型

クラス宣言はインスタンスの型を作る

クラス宣言はクラスのインスタンス変数の型を宣言しており、class名がそのまま型名として使える。

class User {
  name: string = '';
  age: number = 0;

  isAdult(): boolean {
    return this.age >= 20;
  }
}

そのため、以下が成り立つ

const uhyo: User = new User();

これもOK

const john: User = {
  name: "John Smith",
  age: 15,
  isAdult: () => true
};

クラスオブジェクト自体の型

以下のクラスオブジェクトの型はnew () => Userで表現できる。

class User {
  name: string = '';
  age: number = 0;
}

そのため、以下が成り立つ

type MyUserConstructor = new () => User;

const MyUser: MyUserConstructor = User;

//uはUserの型を持つ
const u: User = new MyUser();

instanceofでインスタンスが特定のクラスのインスタンスかどうかを判定できる

//基本系(返り値はboolean)
値 instanceof クラスオブジェクト

型の絞り込みに使える。

type HasAge = {
  age: number;
};

class User {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

function getPrice(customer: HasAge) {
  if (customer instanceof User) {
      //ここに入るということはcustomerはUser型であり、nameプロパティを必ず持っている(型の絞り込み)
    if (customer.name === "uhyo") {
      return 0;
    }
  }
  return customer.age < 18 ? 1000 : 1800;
}

const customer1: HasAge = { age: 15 };
const customer2: HasAge = { age: 40 };
const uhyo = new User("uhyo", 26);


console.log(getPrice(customer1));//1000

console.log(getPrice(customer2));//1800

//UserはHasAgeの部分型なので、引数にできる
console.log(getPrice(uhyo));//0

しかし、TypeScriptではクラスを使わないことも多いので、instanceofを使う機会は少ないそう。「User型のオブジェクトである」ことと、「Userクラスのインスタンスである」ということは必ずしも一致せず、 クラス利用を前提としたinstanceofによる絞り込みは登場機会が少なそうです。

クラスの継承

継承(1)子は親の機能を引き継ぐ

//基本系
クラス名 extends 親クラス { ... }

継承したクラスの型は親クラスの型の部分型になる。

class User {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.#age = age;
  }

  public isAdult(): boolean {
    return this.#age >= 20;
  }
}

//Userを継承し、rankプロパティが増えた
//PremiumUser型はUser型の部分型
class PremiumUser extends User {
  rank: number = 1;
}

//引数はUser型指定
function getMessage(u: User) {
  return `こんにちは、${u.name}さん`;
}

const john = new User("John Smith", 15);
const uhyo = new PremiumUser("uhyo", 26);


console.log(getMessage(john)); //"こんにちは、John Smithさん"と表示される
//PremiumUser型はUserの部分型なのでgetMessage関数が使える
console.log(getMessage(uhyo)); //"こんにちは、uhyoさん"と表示される

継承(2)親の機能を上書きする

親が持つ機能を子クラスで再宣言するとオーバーライドできる

class User {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.#age = age;
  }

  public isAdult(): boolean {
    return this.#age >= 20;
  }
}

class PremiumUser extends User {
  rank: number = 1;

    //ここでisAdultを再宣言してオーバーライド
  public isAdult(): boolean {
    return true;
  }
  
  //返り値の型は変えられない
  //↓はエラーになる
  //public isAdult(): string {
    //return "大人です!";
  //}
}

const john = new User("John Smith", 15);
const taro = new PremiumUser("Taro Yamada", 15);

console.log(john.isAdult()); //falseが表示される
console.log(taro.isAdult()); //trueが表示される

override修飾子を使うとoverrideをしていることが明示的に示せる。使用は必須ではないが、もしオーバーライドになっていなかった場合、コンパイルエラーを出してくれるようになります。
一方、noImplicitOverrideコンパイラオプションを付けることによって、override修飾子を必須にすることができ、これによって「オーバーライドするつもりがないのにオーバーライドしてしまった」といったミスを防ぐことができるようになる。

implementsによるクラスの型チェック

//基本系
class クラス名 implements 型 { ... }

implementsを使うことで、宣言したクラスの型が指定した型の部分型だということが保証できる。

type HasName = {
  name: string;
}

class User implements HasName {
  name: string;
  #age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.#age = age;
  }

  public isAdult(): boolean {
    return this.#age >= 20;
  }
}

以下のようにUserがHasNameの部分型でなければコンパイルエラーになる。

type HasName = {
  name: string;
}

//エラー:nameがない!
class User implements HasName {
  #age: number;

  constructor(age: number) {
    this.#age = age;
  }

  public isAdult(): boolean {
    return this.#age >= 20;
  }
}

まとめ

「プロを目指す人のためのTypeScript入門」 を見ながらTypeScriptのクラスの型と継承についてまとめました。
TypeScriptのクラスの機能を知ることで、クラスを採用するか否かの判断ができるように少しはなったかと思います。
普段使っているRails以外のフレームワークがどのように構成されているのかも気になったので、触ってみようかと思います。

GitHubで編集を提案

Discussion