『リファクタリング(第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
superはECMAScript2015/ES6の仕様なので、tsconfig.jsonのtargetをes6
にしないといけなかった。
=============================
以降、読んだだけでは理解しづらかった箇所をメモ。
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.push
でcourses
が更新できてしまうと、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
を変更できないようにする。