abcdefGets

ゲッツ!

Ecmascriptのprotocolについて

Ecmascriptにprotocolを実装するという提案がある。

proposal-first-class-protocol

もともとGotanda.jsで発表した内容だけどいろいろ追記した。
資料はこれ

speakerdeck.com

内容

元々はinterfaceの提案だった。
それが名前と形を変えてprotocolの提案になった。

protocolとは?

classが実装すべき規約。
interfaceと違って型ではなく規約を提供する。
型クラスとかが近いのかな?

Ecmascript protocol

基本

今回の提案はちょっと特殊な形のprotocolの実装で、
実態はSymbolのコレクションになっている。

例.

protocol ProtocolName {
  // 実装が必要なシンボルを宣言
  thisMustBeImplemented;
}
  
class ClassName implements ProtocolName {
  [ProtocolName.thisMustBeImplemented]() {
    ...
  }
} 

上の実装を見るとわかるけど、protocolのメンバーは自動的にSymbolになる。
そしてprotocolを実装するクラスはそのSymbolを実装しなければいけない。
さらにprotocolは複数実装できる。

例.

protocol A { a; }
protocol B { b; }
  
class ClassName implements A, B {
  [A.a]() {}
  [B.b]() {}
}

またprotocolは実装を持つこともできる。

例.

protocol ProtocolName {
  // 実装が必要なシンボルを宣言
  thisMustBeImplemented;
  
  // メソッド実装
  youGetThisMethodForFree() {
    return this[ProtocolName.thisMustBeImplemented]();
  }
}
  
class ClassName implements ProtocolName {
  [ProtocolName.thisMustBeImplemented]() {
    ...
  }
}

const className = new ClassName();
className.youGetThisMethodForFree(); // ClassNameのインスタンスで呼べる

このprotocolは実装を持っていて、それを実装したクラスは同時に実装も引き継ぐ。
なのでtraitの様に振る舞うことができる。

拡張

さらに既存のクラスも拡張することができる。
たとえばArrayを拡張する場合。

protocol Functor {
  map;
}
  
Promise.prototype[Functor.map] = () => {
  ...
}

// このimplement関数でFunctorを実装したことを宣言
Protocol.implement(Promise, Functor);

このように既存クラスのprototypeを拡張し、implement関数を呼び出すことで、
既存のクラスにもprotocolを適用することができる。

チェック

あるクラスがprotocolを実装しているかどうかは、instanceofでは判定できない。
なので、implements演算子が提案されている。

Promise implements Functor // true

if (MyClass implements SomeProtocol) { }

上記の例のようにimplementsキーワードが演算子のように振る舞う。

extends

protocol自体は既存のprotocolを拡張することができる。

例.

protocol A { a; }
protocol B { b; }
protocol C extends A, B { c; }

こんな感じでprotocolは複数のprotocolを拡張することができる。

デフォルト実装

上記の拡張を利用するとデフォルト実装を持つprotocolを定義することができる。

例.

protocol A { a; }

protocol B extends A {
  [A.a]() { ... }
}

class Class implements B {}
const c = new Class();
c[A.a]();

protocol Bはprotocol Aを拡張し更にaを実装している。
なので、Classインスタンスに対してA.aを呼び出すことが可能になる。

static protocol

protocolはなんと静的プロパティ(!)に対しても作用させられる。

例.

protocol A {
  static b() {}
}
  
class C implements A { }
C[A.b]();

これはどうなんだ...

TypeScript

とりあえず既存のinterfaceは残すであろうとのこと。
protocolをどのような型として扱うかは未定。
Allow dynamic name in typesという型のプロパティ名にsymbolを使うことができるPRが進んでいるので、
これが実装されるとprotocolもスムーズにいけるかも。
ちなみにここで議論しています。

Question: TypeScript and proposal-first-class-protocols · Issue #18814 · Microsoft/TypeScript · GitHub

prototypeへの直接代入について

先程、既存のクラスにprotocolを実装するケースでprototypeへ直接代入するという例を出したが、
prototypeへの直接代入は何か気持ち悪い...(Object.definePropertyすらない時代からjsを書いていたせいなのか?)ので
それはやめてくれ!その代わり既存のクラスをopen-classにして拡張できるようにすれば良くない?
というアバウトな提案をしてみた。

protocol Functor {
  map;
}
  
// We can implements protocol after declaration.
Array implements Functor {
  [Functor.map]: Array.prototype.map
}

こんな感じ。

結果

とりあえず、classがprototypeを隠蔽したのにこれじゃES5時代じゃないか!
って思いを伝えたら。

classはprototypeを隠蔽したわけじゃないよ。prototypeへの代入もES5時代的ではない。
Ecmascriptのモンキーパッチ方法は常にprototypeだし、これからもjsは未来永劫変わらずこの方法なのさ。

とこのような調子で言われた。
まあEcmascriptがprototypeを隠蔽して、もっと 普通 のclassベース言語になりたがってるように見えていた私にとって、
これが目指す世界ならもう言うことは無かったので議論を打ち切った。

でもprototypeはもう触りたくないんじゃ!

おしまい。

HTML5 Conference 2017に登壇します

ひょんなことから

HTML5 Conference

に登壇する機会をいただけることに。

タイトルはDeep dive into TypeScriptということで、TypeScriptの紹介や、RoadMap・Issuesの話とかをさせて頂くつもり

同じ時間帯に任天堂の方のセッションとかあってアレなんですが、TypeScriptに興味のある方はぜひ。

Typescript 2.5 リリース

Typescript 2.5がでたので変更点をメモ

Optional-catch-bindingの導入

catch節のエラーオブジェクトが省略可能になった。
現在Stage3のOptional catch bindingが導入された形に。

サンプル

let input = "...";
try {
    JSON.parse(input);
}
catch {
    // ^ catch節の変数宣言を省略している。
    console.log("Invalid JSON given\n\n" + input)
}

jsにJSDocベースの型アサーションとキャストを導入

javascriptファイルもJSDocコメントで型チェック可能になった。
コメントで@ts-checkをつけるか、
コンパイラオプションでcheckJsオプションをtrueにすれば動作する。

型チェックの詳細は以下のWikiに書いてある。

Type Checking JavaScript Files · Microsoft/TypeScript Wiki · GitHub

重複したパッケージのリダイレクト

簡単に言うとnode_modulesのパッケージの同一性チェックをする。

つまり、moduleResolutionがnodeの場合node_modulesからモジュールをインポートするが、
その際にそのモジュールのpackage.jsonをチェックして、すでにインポートした事のあるモジュールかどうか確認する。
もし同一のモジュールであれば、すでにimport済みのモジュールにリダイレクトして同じモジュールをロードしない。
ちなみにpackage.jsonのversionとnameをチェックして同一性判定をする。

node_modules -
             |
             [A-package] -
             |          |
             |          node_modules -
             |                       |
             |                       [C-package]
             [B-package] -
                         |
                         node_modules -
                                      |
                                      [C-package]

この例だと、A-packageとB-packageが同じC-pacakgeを必要としているが、typescriptコンパイラは一度しかC-packageをインポートしない。
B-packageの必要とするC-pacakgeはA-packageのC-packageを参照する。

–preserveSymlinksコンパイラフラグの導入

–preserveSymlinksオプションが導入される。
このオプションはシンボリックリンクされたモジュールが他のモジュールをimportする場合に、
シンボリックリンク元のパスを起点とした相対パスではなく、
シンボリックリンクが置かれている場所からの相対パスとなる。

A -
  |
  B -
  |  |
  |  C'(Symblink of C)
  C

この例だと、C'モジュールのパス解決は–preserveSymlinksをセットしない場合は、
A/Cから始まることになる。
しかし–preserveSymlinksをセットすると、
A/B/C'からパス解決が行われる。

まとめ

意外と新機能は少なかったですね。

typescript 2.4 の新機能

typescript2.4がでたので新機能を確認。

Dynamic Import Expressionsのサポート

import('...')式がサポートされた。

import式を使うことで多くのバンドラーがコード分割をすることが可能になるので、
module: esnextで出力するのがおすすめだそう。

async function getZipFile(name: string, files: File[]): Promise<File> {
  const zipUtil = await import('./utils/create-zip-file');
  const zipContents = await zipUtil.getContentAsBlob(files);
  return new File(zipContents, name);
}

String Enumsのサポート

待望?の文字列enumがサポートされた。

enum Colors {
  Red = "RED",
  Green = "GREEN",
  Blue = "BLUE",
}

ただし制限として、数値enumの際に可能だった、メンバプロパティからプロパティ名の取得はできない。

Colors[Colors.Red] // これはできない。

interfaceのgeneric型のサポートを強化

戻り値の推論能力の強化

戻り値から型パラメータを導出できるようになった。

function arrayMap<T, U>(f: (x: T) => U): (a: T[]) => U[] {
  return a => a.map(f);
}

const lengths: (a: string[]) => number[] = arrayMap(s => s.length);

Promiseもこのようにエラーとすることができるように。

let x: Promise<string> = new Promise(resolve => {
  resolve(10);
  //      ~~ Error!
});

文脈からの型パラメータの導出

いかのような定義があった場合

let f: <T>(x: T) => T = y => y;

yanyになってしまっていた。
そのため、

let f: <T>(x: T) => T = y => y() + y.foo.bar;

のような式は型チェックをスルーしてしまっていたが、
2.4からyが正しく導出され、エラーとなるようになった。

generic関数の方チェックを厳格化

以下のような関数があった場合、

type A = <T, U>(x: T, y: U) => [T, U];
type B = <S>(x: S, y: S) => [S, S];

function f(a: A, b: B) {
  a = b;  // Error
  b = a;  // Ok
}

AがBと互換性があるかをチェックできるようになり、互換性があれば、代入も可能になった。

コールバック関数の方の反変性チェックを厳格化

2.4以前のバージョンでは以下のプログラムではエラーが起きなかった。

interface Mappable<T> {
  map<U>(f: (x: T) => U): Mappable<U>;
}

declare let a: Mappable<number>;
declare let b: Mappable<string | number>;

a = b;
b = a;

なぜなら、abがmap関数のパラメータfを通して変換可能であると判断されていたため。
しかし2.4以降は実際のabの型を比較を行うため、このプログラムはコンパイルエラーとなる。
この変更は破壊的変更となるので注意

Weak Typesの導入

以下のように、すべてがoptionalな型をWeakTypeとして区別することになった。

interface Options {
  data?: string,
  timeout?: number,
  maxRetries?: number,
}

2.4からはこのWeakTypeに対しても、存在しないプロパティを持つ型を代入するとエラーになる。

function sendMessage(options: Options) {
  // ...
}

const opts = {
  payload: "hello world!",
  retryOnFail: true,
}

// Error!
sendMessage(opts);
// optsとOptions型で一致するプロパティがないためエラー

この変更は破壊的変更となるので注意

現在以下のWorkaroundが提案されている。

  • 実際にプロパティが存在する場合のみ宣言する。
  • WeakTypeには{[propName: string]: {}}のようなインデックス型を定義する。
  • opts as Optionsのように型アサーションを使って変換する。

まとめ

型チェックの強化がメインの変更点になった。
破壊的変更が幾つかありますが、WeakTypeのとこはちょっと注意した方が良さそう。
あとはimport式をどう使うか。

babelのAsyncIterationバグ

問題

for-await-ofのボディで配列への分割代入を行うと、

Cannot read property 'file' of undefined

というエラーを投げてトランスパイルに失敗する。
どうやらscopeの解析に失敗しているらしい。

サンプルコード

async function g(t) {
  return new Promise(r => setTimeout(() => r([true]), t));
}

async function r() {
  for await (const t of [1000, 2000, 3000]) {
    const [result] = await g(t);
  }
}

r();

package.json

"dependencies": {
  "babel-cli": "^6.24.1",
  "babel-plugin-transform-async-generator-functions": "^6.24.1",
  "babel-plugin-transform-regenerator": "^6.24.1",
  "babel-plugin-transform-runtime": "^6.23.0",
  "babel-polyfill": "^6.23.0",
  "babel-preset-es2015": "^6.22.0",
  "babel-preset-stage-3": "^6.24.1",
  "babel-runtime": "^6.23.0"
},
"babel": {
  "plugins": [
    [
      "transform-runtime",
      {
        "helpers": false,
        "polyfill": false,
        "regenerator": true,
        "moduleName": "babel-runtime"
      }
    ]
  ],
  "presets": [
    "es2015",
    "stage-3"
  ]
}

解決

とりあえずbugとしてbabel側にはissueを立てておいた。

github.com

現状で取れる選択肢

配列ではなくオブジェクト形式で受け取る

データ構造の変更が必要だがまあ許容できるか?

async function g(t) {
  return new Promise(r => setTimeout(() => r({result: true}), t));
}

async function r() {
  for await (const t of [1000, 2000, 3000]) {
    const {result} = await g(t);
  }
}

r();

分割代入で受け取らない

その後にインデックスアクセスしなければならないのでちょっと面倒

async function g(t) {
  return new Promise(r => setTimeout(() => r([true]), t));
}

async function r() {
  for await (const t of [1000, 2000, 3000]) {
    const result = await g(t);
  }
}

r();

まとめ

はやく直ってくれると嬉しい

Webpackでpolyfillをちゃんと動かす

面倒だったので備忘録

Polyfillの設定

とりあえずwebpackはrequireしないと駄目。
でも既存のコードを一切変えずにpolyfillをインストールしたい。

webpack.ProvidePluginを使う。

new webpack.ProvidePlugin({
  'Promise': 'es6-promise',
  'Symbol': 'es6-symbol',
  'fetch': 'imports?this=>global!exports?global.fetch?window.fetch!whatwg-fetch',
  'Response': 'imports-loader?this=>global!exports-loader?global.Response!whatwg-fetch'
}),,

こんな感じで書いた。

次に

npm i imports-loader exports-loader -Dを実行。

Promise、Symbolはrequire('es6-promise')require('es6-symbol')がそれぞれPromiseSymbolという変数名でモジュール毎に定義される。
fetchはとても読みづらいし考えたやつ頭おかしいが、
まず、whatwg-fetchの定義の下のthisglobalを参照している箇所でmodule.exports = fetchgを定義して、
fetchを参照しているモジュールに(function(fetch){ ... })(require('whatwg-fetch'))のようなコードを追加する。
Responseも同じく。

まとめ

webpackの独自仕様を追加すぎるとESModuleへの移行がつらそうなので程々に。

jspmからWebpackに移行した

とりあえず、Webpackを導入したがそのままだと色々問題が多かったので以下の事をやった。
まあ今更感あるが。

  • typescriptのallowSyntheticDefaultImportsをfalseにする。
  • production用とdev用のconfigをいい感じにわける
  • node_modulesのdllを生成する。

typescriptのallowSyntheticDefaultImportsをfalse

今回はtypescriptとwebpackの組み合わせだったのだが、webapckに移行する前にはjspmを利用していたので、

allowSyntheticDefaultImports: true,
module: system,

で運用していた。ちなみにこのallowSyntheticDefaultImportsが何かというと、
本来typescriptのES6 Moduleはexport default ...の構文しか、直接importできない。

// a.js
module.exports = function() {}
// b.ts
import fn from './a'; // Error
import * as fn from './a'; // OK

しかし、allowSyntheticDefaultImportstrueにすると、commonjsスタイルのmoduleも直接importできるようになる。
のだが、webpackはsystemjsを取り扱えず、module: commonjsにしたところ、このallowSyntheticDefaultImportsがうまく動作しなくなった。

// a.js
module.exports = function() {}
// b.ts
import fn from './a';

fn(); // Error

ので、allowSyntheticDefaultImportsfalseにして、すべてimport * as ...の方式で読み込むことにした。

production用とdev用のconfigをいい感じにわける

まあ、webpack.config.js自体がnodejsのモジュールだったので、webpack.config.jsに設定を書いてもいいのだが、
凄くごちゃごちゃするので、webpack.dev.config.jsを作って、そこでwebpack.config.jsを読み込み、
_.cloneで設定をコピーしてdev用の設定を追加するようにした。

node_modulesのdllを生成する。

コンパイルがあまりに遅かったので、DllPluginを利用して、変更されない外部モジュールを全てまとめておいた。

dll側

output: {
  filename: '[name].dll.js',
  library: 'vendor_library' // Bundleが定義されるグローバル変数名。参照する時に使用する。
},
plugins: [
  new webpack.DllPlugin({
    path: path.join(__dirname, 'dll', '[name]-manifest.json'), // manifestファイルを出力する。とりあえず-manifestにしとけばok
    name: 'vendor_library' // output.libraryで指定した名前
  })
]

使う側

plugins: [
  ...
  new webpack.DllReferencePlugin({ // Dllを参照する
    context: __dirname,
    manifest: require('./dll/vendor.production-manifest.json') // 上のmanifestファイルへのパス
  })
]

こうすることでnode_modulesとかのあまり変化しないファイルをコンパイルしておくことができ、
そのコンパイルしたファイルを参照することでコンパイル時間を短縮できる。
ただし、DllReferencePluginはconcatしてくれないので、dllは手動でconcatするか、別途scriptで読み込む必要がある。
今回はconcatした。

gulp.task('minify', done => {
  const webpack = require('webpack');
  const config = _.clone(require('./webpack.config.js'));
  config.output = _.clone(config.output);
  config.output.path = path.join(__dirname, DIST);
  const compiler = webpack(config);
  compiler.run((err, stats) => {
    if (err) {
      throw err;
    }
    console.log(stats.toString('minimal'));
    try {
      const main = `${DIST}/main.js`;
      const app = fs.readFileSync(main, 'utf8');
      const vendor = fs.readFileSync(`dll/vendor.production.dll.js`, 'utf8');
      fs.writeFileSync(main, `!function(){${vendor};\n${app}}();`, 'utf8');  //ここでconcatしてる。
    } catch(e) {
      throw e;
    }
    done();
  });
});

さらに新たにnode_modulesがロードされても更新されるように、package.jsonにも以下の記述をした。

"scripts": {
  "postinstall": "node ./node_modules/.bin/gulp bundle-dll"
}

これでnpm install/yarn addしてもbundleが更新される。

あと、はまった点として、予めbundleしておく中にreactとかも入れていたので、process.env.NODE_ENVの値に困ってしまった。
なので、dllファイルをdev用、production用と2つ用意して対処した。

function bundleDll(env, done) {
  const webpack = require('webpack');
  const config = _.clone(require('./webpack.dll.config.js'));
  config.entry = _.clone(config.entry);
  config.output = _.clone(config.output);
  config.output.path = path.join(__dirname, 'dll');
  config.plugins = config.plugins.slice();

  const vendor = config.entry.vendor;
  const keyName = `vendor.${env}`;
  config.entry = {};
  config.entry[keyName] = vendor;
  
  if (env === 'production') {
    config.plugins.push(new webpack.optimize.UglifyJsPlugin());
  }
  config.plugins.push(new webpack.DefinePlugin({
    'process.env.NODE_ENV': `'${env}'`
  }));
  const compiler = webpack(config);
  compiler.run((err, stats) => {
    if (err) {
      throw err;
    }
    console.log(stats.toString('minimal'));
    done();
  });
}

/**
 * dev dll
 */
gulp.task('bundle-dev-dll', done => {
  bundleDll('development', done);
});


/**
 * production dll
 */
gulp.task('bundle-prod-dll', done => {
  bundleDll('production', done);
});


gulp.task('bundle-dll', () => {
  const rs = require('run-sequence');
  return rs(
    'clean-dll',
    'bundle-dev-dll',
    'bundle-prod-dll'
  );
});

まとめ

jspmよりwebpackの方が快適でした。
さよならjspm