経験は何よりも饒舌

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を変更できないようにする。

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関数について見ていこうと思う。

wafuwafu13.hatenadiary.com

Linuxを購入

のが届いた。Macの5倍厚い。1万5000円。Ubuntu 16.04。

前からこういう本をやりたかったけど、Macだと仮想環境を用意しないといけない。

その仮想環境がそもそもどう構築されているのか分からないし、慣れていない仮想環境で理解できる自信がなかったので、買った方が早いと思った。
あとなんとなくLinuxに慣れたいというのもあった。ついでにvimも。

上の本をやる前に、まずこれをやりはじめた。
図解がわかりやすく、コンピュータの中身を掴めた感じがした。

1/10追記
これをやり終えた。
脳内に散らばっていたネットワークの知識が集約されていく感じがした。
TCP/IP等の説明に抵抗感が全くなくなった。

この本も同時期に読んだのも、DNSという観点からネットワークを俯瞰することができて理解に拍車がかかった。

jQueryはいかにしてDOMを取得するか(3)

wafuwafu13.hatenadiary.com

いよいよ今回は、以下の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"であり、contextrootundefinedであった。

よって、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)

wafuwafu13.hatenadiary.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

今回は、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 ) {...

ここで、引数であるselectorcontextrootには何が入っているのかを調べると、
selectorは、

#document {location: Location, implementation: DOMImplementation, URL: "file:///Users/tagawahirotaka/Desktop/index.html",....

であり、contextrootundefinedであった。

よって、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であった。

DocumentNodeを継承しており、Node.nodeType9は、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;

thisjQuery.fn.initのことで、そこにselectorが挿入されている。

しかし、console.log($())の出力を見てみると、

▽jQuery.fn.init {}
  ▷__proto__: Object(0)

となっているため、後続の処理で破棄されている。

ちなみに、thisjQuery.fn.initである理由は、生成されるインスタンス自身がthisにセットされるからである。

function foo() {
    console.log(this) // ▽ foo {}
                //  ▷ __proto__: Object
}

var bar = new foo()


wafuwafu13.hatenadiary.com

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);としても同じ結果が得られる。

wafuwafu13.hatenadiary.com

【DMM × CAMPHOR-】Webフロントエンド開発トークセッション参加メモ

camphor.connpass.com

トーク1: 横断組織から見るDMMのフロントエンド

  • jQueryとの付き合い
    • 技術強い会社でもあるあるなんだなぁ
    • jQueryに稼いでもらってる側面もあるあるかも
  • ドキュメント
    • コードのノウハウが細かくドキュメント化されていた
    • フロントエンドの評価項目がとても細かかった ex)SOLID原則の理解
  • フロントエンドエンジニア
    • フロントに限らずバックからインフラまで触れる風潮
    • 逆にフロント一本で他人と差別化するのは難しいのではと思った
  • CTO室
    • いろいろやってるみたい

トーク2:生まれ変わるDMM〇〇

  • 経験
    • リプレイスの知見を1つの会社で短期スパンで異なるプロジェクトに転用できるのは事業範囲がやたら広い会社でしか無理なのではと思った
  • 技術選定
    • Next.jsじゃなくReact にした
      • Next.jsは表示速度が速いは眉唾
      • SSRSEOの強みはCSRでもDynamic RenderingやLambda絡めたらいけるっぽい
      • SSRでのAPIコールは必要なかった
      • コードの書き分け、開発環境と本番環境の差異がだるい
      • Node.jsが重くて落ちる
      • GraphQLはノリと挑戦

トーク3: ビデオ通話システムに関するFE技術まわりの話

  • 開発環境
    • TS化を促す
      • チームで整合性とれる
      • null安全
      • 段階的にできる
    • Sentryでのフロントエンド監視は眉唾
    • Storybook良いが管理大変
  • 困りごと
    • 自動再生ができない
      • ハックしてがんばる