経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

『リファクタリング(第2版): 既存のコードを安全に改善する』をTypeScriptで実装

最近、フロントエンドのコードを整理する機会が増えてきたので、ただ型をつけるだけではなくて、構造を捉えたリファクタリングができるようになりたいと思い、進めている。

第2版のサンプルコードはJavaからJavaScriptになっており、せっかくなのでTypeScriptを導入することにした。
1章まで終え、関数の抽出や、ポリモーフィズムによる条件記述の置き換えなど、学ことは多かったが、ここでは、型をつけるにあたって少し詰まったところを列挙していく。

リポジトリはこちら
github.com

JSONの読み込み

TypeScript導入 · wafuwafu13/Refactoring-Improving-the-Design-of-Existing-Code@4483210 · GitHub

--resolveJsonModuleオプションをつければ自動で型がつくようになる。

今回は、.d.tsファイルを作成して自分で型定義をした。

mapの返り値の型

27 · wafuwafu13/Refactoring-Improving-the-Design-of-Existing-Code@56125ae · GitHub

以下のように、mapで生成する新しい配列の中のオブジェクトに新たなプロパティがたされる場面があった。

const invoice = {
  customer: "BigCo",
  performances: [
    {
      playID: "hamlet",
      audience: 55,
    },
    {
      playID: "as-like",
      audience: 35,
    },
    {
      playID: "othello",
      audience: 40,
    },
  ],
};

const statementData = {};

statementData.customer = invoice.customer;

statementData.performances = invoice.performances.map((aPerformance) => {
  const result = Object.assign({}, aPerformance);
  result.play = "hamlet";
  return result;
});

まず、この状態だと、statementData.customerの部分に、Property 'customer' does not exist on type '{}'.というエラーが出る。
これは、statementDataの型が{}であるからだ。
よって、型を定義する。

type StatementData = {
    customer?: string
}

const statementData: StatementData = {};

statementData.customer = invoice.customer;

customer?をつけることにより、

const statementData: StatementData = {};

としたときに、Property 'customer' is missing in type '{}' but required in type 'StatementData'.というエラーが出ないようにする。

続いて、

statementData.performances = invoice.performances.map((aPerformance) => {
  const result = Object.assign({}, aPerformance);
  result.play = "hamlet";
  return result;
});

で、Property 'performances' does not exist on type 'StatementData'.のエラーが出ないように、ひとまずanyで型定義をしておく。

type StatementData = {
    customer?: string
    performances?: any
}

すると、result.play = "hamlet";の部分に、Property 'play' does not exist on type '{ playID: string; audience: number; }'.というエラーが出る。

これは、const result = Object.assign({}, aPerformance);で定義されたresultの型が

const result: {
    playID: string;
    audience: number;
}

であるからだ。

なぜresultの型がそうなるのかというと、invoice.performancesの型が以下であり、

(property) performances: {
    playID: string;
    audience: number;
}[]

aPerformanceの型がresultに反映されているからだ。

よって、aPerformanceの型を定義するために、先ほどanyにしていた部分をきちんと定義する。

type StatementData = {
  customer?: string;
  performances?: {
    playID: string;
    audience: number;
    play?: string;
  }[];
};

そして、このperformancesの型をIndexed Access Typesを用いてaPerformanceに当てていく。

statementData.performances = invoice.performances.map(
  (aPerformance: StatementData["performances"][0]) => {
    const result = Object.assign({}, aPerformance);
    result.play = "hamlet";
    return result;
  }
);

しかし、StatementData["performances"][0]の部分で、Property '0' does not exist on type '{ play?: string | undefined; }[] | undefined'.のエラーが出る。

これは、StatementData["performances"]undefinedの可能性があるからであり、Requiredを用いてその可能性を潰せばよい。

statementData.performances = invoice.performances.map(
  (aPerformance: Required<StatementData>["performances"][0]) => {
    const result = Object.assign({}, aPerformance);
    result.play = "hamlet";
    return result;
  }
);

そうすることで、resultの型は以下のようになる。

const result: {
    playID: string;
    audience: number;
    play?: string | undefined;
}

最終的なコードは以下のようになる。

const invoice = {
  customer: "BigCo",
  performances: [
    {
      playID: "hamlet",
      audience: 55,
    },
    {
      playID: "as-like",
      audience: 35,
    },
    {
      playID: "othello",
      audience: 40,
    },
  ],
};

type StatementData = {
  customer?: string;
  performances?: {
    playID: string;
    audience: number;
    play?: string;
  }[];
};

const statementData: StatementData = {};

statementData.customer = invoice.customer;

statementData.performances = invoice.performances.map(
  (aPerformance: Required<StatementData>["performances"][0]) => {
    const result = Object.assign({}, aPerformance);
    result.play = "hamlet";
    return result;
  }
);
superで親のメソッドを呼び出す

41 · wafuwafu13/Refactoring-Improving-the-Design-of-Existing-Code@a4293c7 · GitHub

superECMAScript2015/ES6の仕様なので、tsconfig.jsontargetes6にしないといけなかった。


=============================

以降、読んだだけでは理解しづらかった箇所をメモ。

p178 コレクションのカプセル化
class Person {
  constructor(name) {
    this._name = name;
    this._courses = [];
  }
  get name() {
    return this._name;
  }
  get courses() {
    return this._courses;
  }
  set courses(aList) {
    this._courses = aList;
  }
}

class Courses {
  constructor(name, isAdvanced) {
    this._name = name;
    this._isAdvanced = isAdvanced;
  }
  get name() {
    return this._name;
  }
  get isAdvanced() {
    return this._isAdvanced;
  }
}

const aPerson = new Person("foo");

aPerson.courses.push(new Courses("bar", false));
console.log(aPerson.courses); // [ Courses { _name: 'bar', _isAdvanced: false } ]

aPerson.courses.pushcoursesが更新できてしまうと、Personクラスは変更をコントロールできていない、つまり、setterが機能していない。

class Person {
  constructor(name) {
    this._name = name;
    this._courses = [];
  }
  get name() {
    return this._name;
  }
  get courses() {
    return this._courses.slice();
  }
  addCourse(aCourse) {
    this._courses.push(aCourse);
  }
}

const aPerson = new Person("foo");

aPerson.addCourse(new Courses("bar", false));
aPerson.courses.push(new Courses("hoge", true))
aPerson.courses.push(new Courses("fuga", true))

console.log(aPerson.courses); // [ Courses { _name: 'bar', _isAdvanced: false } ]

addCourseメソッドをPersonクラスに定義し、getterはthis._courses.slice()によりコピーを返すことで、aPerson.courses.pushといった方法でcoursesを変更できないようにする。