iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

TypeScriptのclassってなんだ?

はじめに

はじめまして!株式会社iimonにてフロントエンドエンジニアをしております、ガジェットおじさんです! 業務では主にTypeScriptの型と睨めっこをしています!

TypeScript書いてますか?静的型付け言語はいいぞ!
文字通り型をつけられるので事故が減ったり、予測が容易になったりとメリットが多い。
一度、型を使ったらもう素のJavascriptなんて恐ろしくて使えない……。 今回はそんな中毒性のある言語であるTypeScriptにおけるクラスについて書いていこうかなと思います!

classってなんだ?

さて、突然ですが、クラスといえばなんでしょう? 学校のホームルームの単位?某ソルジャークラス1stの人? ……冗談はさておき、クラスというと次のようなものを思い浮かべるかと思います。

/** apiからの返り値をフォーマット */
class Deserializer<T> {
    deserializer(input: T) {
        return this;
    }
}
/** 従業員情報 */
type EmployeeModelType = {
  name: string;
  email: string;
  tel: string;
}
class EmployeeModel extends Deserializer<EmployeeModel> implements EmployeeModelType {
  name: string = '';
  email: string = '';
  tel: string = '';
  private emailValidateReg = /^[a-zA-Z0-9_.+-]+@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/;

  deserializer(input: EmployeeModelType): this {
    this.name = input.name;
    this.email = this.validateEmail(input.email);
    this.tel = input.tel;
    return this;
  }
  /** メールアドレスの妥当性チェック */
  private validateEmail(email: string) {
    return this.emailValidateReg.test(email) ? email : '';
  }
}
/** 会社情報 */
type CompanyModelType = {
  name: string;
  email: string;
  tel: string;
  employees: EmployeeModelType[];
}
class CompanyModel extends Deserializer<CompanyModel> implements CompanyModelType {
  name: string;
  email: string;
  tel: string;
  employees: EmployeeModelType[];

  deserializer(input: CompanyModelType): this {
    this.name = input.name;
    this.email = input.email;
    this.tel = input.tel;
    this.employees = input.employees.map(employee => new EmployeeModel().deserializer(employee));
    return this;
  }
}

ここでは会社情報と従業員情報をdeserializeするクラスを宣言しました。 apiを介して会社情報を型付きで取得することを想定してます。

会社情報の中に、従業員情報を複数持つものとして使われていることが分かるかと思います。 ただ、従業員情報もフォーマットする必要性が出てくるかもしれません。

input.employees.map(employee => new EmployeeModel().deserializer(employee));

ここで、会社情報に含まれる従業員情報をdeserializeしているため、より安全なデータを取得しています。 あるいはDBに登録されている更新日時等をYYYYMMDD形式に変換してから格納する等の小技もできます!

続いて次のコードはどうでしょう?

type PostCodeType = {
  address: string;
}
/** 住所情報から郵便番号や関東圏か判定を行うクラス */
class PostCode implements PostCodeType {
  address: string;
  zipCode: string;
  isKanto: boolean;

  constructor(initData: PostCodeType) {
    this.address = initData.address;
    this.convertAddressToZipCode();
    this.setIsKanto();
  }

  private setZipCode() {
    this.zipCode = this.convertAddressToZipCode();
  }
  private convertAddressToZipCode() {
    // ~省略~
    return result;
  }
  private setIsKanto() {
    const area = this.checkArea(this.address);
    this.isKanto = area === 'kanto';
  }
  private checkArea(): string {
    // ~省略~
    return result;
  }
}

こちらは住所を基に、郵便番号と関東圏の判定もオブジェクトにセットして、運用しようと強引にクラス化したものになります。 正直、個別に関数として宣言しても問題ないはずです。 クラス名からメソッドチェーンのように使いたいのであれば、namespaceを使うことをお勧めします!

namespaceとは

元々は内部モジュールと呼ばれていたようで、コードを構造化する際に名前をつけられるようにするものです。 実際に先ほどのコードをこれに置き換えてみます。

export namespace PostCode {
  /** 住所から郵便番号を取得 */
  export const getZipCode = (address: string): string => {
    // convertAddressToZipCodeに記載していた処理
    return result;
  }
  const checkArea = (address: string) => {
    // checkAreaに記載していた処理
    return result;
  }
  /** 住所から関東圏か判定 */
  export const isKanto = (address: string): boolean => {
    const area = checkArea(address);
    return area === 'kanto';
  }
}

クラス化はされていませんが、次のようにアクセスできます。

import { PostCode } from './post_code.ts';
const zipCode = PostCode.getZipCode(address);

このようにnamespaceで構造化できました。 クラス化するものは、オブジェクトとして再利用できるものに限定した方がいいのかなと個人的に思いました。

implementsとextendsについて

改めて最初に書いたコードを見直してみましょう! implements, そしてextendsなるものがありますね。

implementsはクラスが宣言するインターフェースとなります。 クラスに初期値(メンバ変数)を設けること、結構あると思います。 その際に、implementsで定義を付与することで、内包するメンバ変数が宣言されていない時に警告してくれます。 誰も置いてけぼりにしないという強い意志を持ったいい奴なんです、彼は。

extendsはそのままの意味ですが、拡張ですね。 extendsを付けることで拡張元のクラスの機能を使うことができるようになります! 拡張せずに全てのクラスに直接書いていて、それを一括で更新……みたいな仕様変更があった際に役に立ちます。 例えば、更新日時と削除日時しかなかったものに、作成日時、削除フラグもDBとして持つので、これも追加して欲しい!という仕様変更、もとい要望があった際に修正コストが大幅に減ります。

typeとinterface

次は似ていてややこしいのもシリーズとしてお馴染みのtypeとinterfaceついて改めて調べていきます! いきなりの結論ですが、どちらも型を宣言するという用途ではほぼ同じです。

type EmployeeType = {
  name: string;
  address: string;
  email: string;
  tel: string;
}
interface EmployeeIF {
  name: string;
  address: string;
  email: string;
  tel: string;
}

さて、違いはどこにあるのかというと、型宣言として普通に使う分には差はほぼないです。 あるとしたら、仕様の部分が少し異なります。 interfaceが簡単に継承できるのに対し、typeは継承そのものができないようになっています(似たような表現は可能)。 interfaceはinterface, typeを継承できるのに対し、typeは交差型を使わないと継承っぽいことができないといった具合です。

そのため弊社ではtypeを使うように制約を設けてます。 自由度を下げることで、制約が生まれ、なるべく皆が理解しやすいコードを書くことに繋がっているのかなと考えてます!

private, public, staticってなんだ?

class本体についてまとめてきましたが、最後にメソッドについて書いていきます!

privateとpublicに関しては、字面そのままの意味です。 privateメソッドは外部から呼ぶことができなくなります。

用途としては、class内で完結する処理を書く際に使うことが多いと思います。

publicに関しては、何も宣言しない場合はpublicとなるので、意識して使うことはないかと思います。 ただ、たまには彼のことを思い出してあげてください笑

さて、3つ目のstaticですが、個人的にあまり使ってこなかったので、イマイチ用途にビビッとしませんでした。 ただ、ある条件下でとんでもない効力を発揮することが分かりました。 それは、絶対初期化を通さないと次に進めないclassを作りたい時と初期化で非同期処理をしたい時です。

import { CompanyModel, EmployeeModelType } from './Models/company.model.ts';
export class CompanyDialog {
  private name: string = '';
  private email: string = '';
  private tel: string = '';
  private employees: EmployeeModelType[] = [];

  static async init(){
    const companyDialog = new CompanyDialog();
    companyDialog.createCompanyDialog();
    await companyDialog.fetchCompanyData(companyDialog);
    return companyDialog;
  }
  /** 会社情報を取得 */
  private async fetchCompanyData(companyDialog: CompanyDialog): Promise<void> {
    const res = await fetch('会社情報取得api');
    const json = await res.json();
    const companyData = new CompanyModel().deserialize(json);
    companyDialog.name = companyData.name;
    companyDialog.email = companyData.email;
    companyDialog.tel = companyData.tel;
    companyDialog.employees = companyData.employees;
  }
  /** ダイアログを生成 */
  private createCompanyDialog(): void {
    // ~省略~
  }
  /** ダイアログを表示 */
  public showCompanyDialog(): void {
    // ~省略~
  }
}

constructorにasync噛ませればいけると思った、そんな過去が私にもありました。 お分かりの通り、constructorでは使えないんです。 そのため、少し小細工をしてやる必要性が出てきます。 そこで使うのがstaticメソッドというわけです。 staticメソッドとはインスタンスを生成することなく使用できるメソッドのことで、これを利用してinit()の中でインスタンスを生成してしまおう!という魂胆です!

流れとしては、以下のことをやっています。 1. init()の中でCompanyDialogインスタンスを生成。 2. CompanyDialogクラスにある非同期処理fetchCompanyDataを実行。 3. 最後に生成したCompanyDialogインスタンスを返す。

これによって以下のような呼び出しが可能になります!

import { CompanyDialog } from './company_dialog.ts';
const companyDialog = await CompanyDialog.init();
companyDialog.showCompanyDialog();

これで初期化で非同期処理を行いたい場合でも安心ですね!

まとめ

今回はクラスってなんだ?と関連する項目を掘り下げてみました! まだまだ使いこなせていないものもあるので、適材適所、しっかりと見極めて実戦もとい実践していければと思います!

最後までご覧いただきありがとうございました!

弊社はエンジニアを募集しています!

株式会社iimonの会社情報 - Wantedly 株式会社iimonの中途採用/求人 | 転職サイトGreen(グリーン)

参考

TypeScriptのnamespaceは非推奨ではない #TypeScript - Qiita 名前空間(namespace) | TypeScript 日本語ハンドブック | js STUDIO interfaceとtypeの違い | TypeScript入門『サバイバルTypeScript』