『リファクタリング(第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
を変更できないようにする。
Reactのコードを読む(1)
この前jQueryのコードを読んでなんとなくコードリーディングがわかってきたので今回はReactのコードを読んでいこうと思う。
wafuwafu13.hatenadiary.com
直近の目標は、以下のコードでどのようにして画面にhello world
が表示されるかを解明することである。
<!DOCTYPE html> <html> <head> <script src="https://unpkg.com/react@17/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.js"></script> </head> <body> <div id="root"> </div> <script type="text/babel"> ReactDOM.render( <h1>hello world</h1>, document.getElementById('root') ); </script> </body> </html>
コードは今回も行数がわかりやすいようにGitHubにあげた。
github.com
まずは、ReactDOM
が何なのかから見ていこうと思う。
<!DOCTYPE html> <html> <head> <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> </head> <body> <div id="root"> </div> <script> console.log(ReactDOM) </script> </body> </html>
このコードの出力結果は、以下のようなエラーである。
Uncaught TypeError: Cannot read property '__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED' of undefined at react-dom.development.js:15 at react-dom.development.js:12 at react-dom.development.js:13
react-dom.development.js
の15行目を見てみると、以下のような記述がある。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L15
var ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
この__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
は、react.development.js
の3533行目でエクスポートされている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react.development.js#L3533
exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactSharedInternals$1;
よって、react.development.js
も読み込まないといけない。
<!DOCTYPE html> <html> <head> <script src="https://unpkg.com/react@17/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> </head> <body> <div id="root"> </div> <script> console.log(ReactDOM) </script> </body> </html>
今回の出力結果は、以下のようになる。
▽{__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {…}, createPortal: ƒ, findDOMNode: ƒ, flushSync: ƒ, hydrate: ƒ, …} ▷createPortal: ƒ createPortal$1(children, container) ▷findDOMNode: ƒ findDOMNode(componentOrElement) ▷flushSync: ƒ flushSync(fn, a) ▷hydrate: ƒ hydrate(element, container, callback) ▷render: ƒ render(element, container, callback) ▷unmountComponentAtNode: ƒ unmountComponentAtNode(container) ▷unstable_batchedUpdates: ƒ batchedUpdates$1(fn, a) ▷unstable_createPortal: ƒ unstable_createPortal(children, container) ▷unstable_renderSubtreeIntoContainer: ƒ renderSubtreeIntoContainer(parentComponent, element, containerNode, callback) ▷version: "17.0.1" ▷__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {Events: Array(7)} ▷__proto__: Object
これらは、react-dom.development.js
の26280~26290行目でエクスポートされている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L26280
exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = Internals; ... exports.render = render; ...
次回から、このrender
関数について見ていこうと思う。
Linuxを購入
— わふわふ (@wafuwafu13_) 2020年11月22日
のが届いた。Macの5倍厚い。1万5000円。Ubuntu 16.04。
前からこういう本をやりたかったけど、Macだと仮想環境を用意しないといけない。
その仮想環境がそもそもどう構築されているのか分からないし、慣れていない仮想環境で理解できる自信がなかったので、買った方が早いと思った。
あとなんとなくLinuxに慣れたいというのもあった。ついでにvimも。
上の本をやる前に、まずこれをやりはじめた。
図解がわかりやすく、コンピュータの中身を掴めた感じがした。
1/10追記
これをやり終えた。
脳内に散らばっていたネットワークの知識が集約されていく感じがした。
TCP/IP等の説明に抵抗感が全くなくなった。
この本も同時期に読んだのも、DNSという観点からネットワークを俯瞰することができて理解に拍車がかかった。
jQueryはいかにしてDOMを取得するか(3)
いよいよ今回は、以下のHTMLを用意して実際にDOMを取得していく。
<!DOCTYPE html> <html> <head> <script src="https://code.jquery.com/jquery-3.5.1.js"></script> </head> <body> <h1 id="hello">hello world</h1> <script> console.log($('#hello')) </script> </body> </html>
まずは前回と同じように、3133行目に定義されている関数の引数に何が入っているのかを調べる。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3133
jQuery.fn.init = function( selector, context, root ) {...
すると、selector
は"#hello"
であり、context
、root
はundefined
であった。
よって、selector
はstring型であるため、3146行目の条件式はtrue
になる。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3146
if ( typeof selector === "string" )
次の3147行目の条件式は、selector[ 0 ]
が#
であるため、false
である。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3147
if ( selector[ 0 ] === "<" && ...
続く3155行目で、変数match
に値が代入されている。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3155
match = rquickExpr.exec( selector );
ここで使われているrquickExpr
は、3131行目で定義されているRegExpである。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3131
rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
そして、RegExp.prototype.exec()を用い、結果として以下のような配列を返し、変数match
に代入されている。
["#hello", undefined, "hello", index: 0, input: "#hello", groups: undefined]
よって、3159行目の条件式はtrue
になる。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3159
if ( match && ( match[ 1 ] || !context ) ) {
match[1]
はundefined
であるため、3162行目の条件式はfalse
である。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3162
if ( match[ 1 ] ) {
続く3192行目で、Document.getElementById()によりDOMが取得されている。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3192
elem = document.getElementById( match[ 2 ] );
document.getElementById("hello")
の返り値はElementオブジェクトであり、以下のような値である。
h1#hello {align: "", title: "", lang: "", translate: true, dir: "", …},
続く3194行目からは、前回と同じ処理がなされている。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3194
if ( elem ) { // Inject the element directly into the jQuery object this[ 0 ] = elem; this.length = 1; } return this;
よって、console.log($('#hello'))
で以下の値が得られた。
▽jQuery.fn.init [h1#hello] ▷0: h1#hello length: 1 ▷__proto__: Object(0)
今回でjQueryはいかにしてDOMを取得するかが解明できた。
次は違うコードを読んでいきたい。
jQueryはいかにしてDOMを取得するか(2)
前回は、以下のHTMLを用意し、
<!DOCTYPE html> <html> <head> <script src="https://code.jquery.com/jquery-3.5.1.js"></script> </head> <body> <script> console.log($); </script> </body> </html>
以下の実行結果を得た。
ƒ ( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' // Need init if jQuery is called (just allow error to be thrown if not included) return new jQuery…
これは、153行目で定義されている定数であった。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L153
今回は、console.log($())
とすることで、関数の実行結果、つまり157行目の、
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L157
jQuery.fn.init( selector, context )
の実行結果を見ていきたい。
関数は、3133行目に定義されている。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3133
jQuery.fn.init = function( selector, context, root ) {...
ここで、引数であるselector
、context
、root
には何が入っているのかを調べると、
selector
は、
#document {location: Location, implementation: DOMImplementation, URL: "file:///Users/tagawahirotaka/Desktop/index.html",....
であり、context
、root
はundefined
であった。
よって、selector
は、Documentであることがわかる。
selector
が未定義ではなく、string型でもないため、3137行目と3146行目の条件分岐はスルーされる。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3137
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3146
if ( !selector ) ... if ( typeof selector === "string" )
続く3214行目には以下のような条件分岐がある。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3214
else if ( selector.nodeType )
selector.nodeType
を調べてみると、値は9
であった。
DocumentはNodeを継承しており、Node.nodeTypeの9
は、DOCUMENT_NODE
を意味している。
この3215行目からの条件分岐内で、以下のような処理がなされる。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L3215
this[ 0 ] = selector; this.length = 1; return this;
this
はjQuery.fn.init
のことで、そこにselector
が挿入されている。
しかし、console.log($())
の出力を見てみると、
▽jQuery.fn.init {} ▷__proto__: Object(0)
となっているため、後続の処理で破棄されている。
ちなみに、this
がjQuery.fn.init
である理由は、生成されるインスタンス自身がthis
にセットされるからである。
function foo() { console.log(this) // ▽ foo {} // ▷ __proto__: Object } var bar = new foo()
jQueryはいかにしてDOMを取得するか(1)
最近、バイトの隙間時間に、jQueryを廃止してTypeScriptにリプレイスする、というタスクをするようになった。
jQueryのDOMの取得を、Documentを使って書き換える時に、jQueryはいかにしてDOMを取得するか、ということが気になったので少し調べていく。
今回は、そもそもjQueryの$
はなんなのか、ということを調べた。
jQueryのコードは、https://code.jquery.com/jquery-3.5.1.jsから入手し、行数を分かりやすくするためにここに置いた
github.com
さっそく、以下のHTMLを用意して、コードを見ていく。
<!DOCTYPE html> <html> <head> <script src="https://code.jquery.com/jquery-3.5.1.js"></script> </head> <body> <script> console.log($); </script> </body> </html>
このコードの実行結果は、以下のようになる。
ƒ ( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' // Need init if jQuery is called (just allow error to be thrown if not included) return new jQuery…
この実行結果は、153行目で定義されている。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L153
// Define a local copy of jQuery jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' // Need init if jQuery is called (just allow error to be thrown if not included) return new jQuery.fn.init( selector, context ); };
実際に返されている部分は、最後の10871行目である。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L10871
return jQuery;
これがなぜconsole.log($);
で出力できるかというと、10865行目に以下のような処理をされているからである。
https://github.com/wafuwafu13/jquery-3.5.1/blob/master/jquery-3.5.1.js#L10865
window.jQuery = window.$ = jQuery;
だから、console.log(jQuery);
としても同じ結果が得られる。
【DMM × CAMPHOR-】Webフロントエンド開発トークセッション参加メモ
トーク1: 横断組織から見るDMMのフロントエンド
- ドキュメント
- コードのノウハウが細かくドキュメント化されていた
- フロントエンドの評価項目がとても細かかった ex)SOLID原則の理解
- フロントエンドエンジニア
- フロントに限らずバックからインフラまで触れる風潮
- 逆にフロント一本で他人と差別化するのは難しいのではと思った
- CTO室
- いろいろやってるみたい
トーク2:生まれ変わるDMM〇〇
- 経験
- リプレイスの知見を1つの会社で短期スパンで異なるプロジェクトに転用できるのは事業範囲がやたら広い会社でしか無理なのではと思った
トーク3: ビデオ通話システムに関するFE技術まわりの話
- 開発環境
- TS化を促す
- チームで整合性とれる
- null安全
- 段階的にできる
- Sentryでのフロントエンド監視は眉唾
- Storybook良いが管理大変
- TS化を促す
- 困りごと
- 自動再生ができない
- ハックしてがんばる
- 自動再生ができない