Subscribed unsubscribe Subscribe Subscribe

Hatena Developer Blog

はてな開発者ブログ

Scala 関西 Summit ではてなにおけるマイクロサービスと Scala について発表します

こんにちは、アプリケーションエンジニアの id:aereal です。

summit.scala-kansai.org

来たる10月8日に Scala 関西 Summit という関西最大級の Scala カンファレンスが大阪で催されます。

このカンファレンスにおいて、「はてなにおけるマイクロサービスと Scala」と題してはてなにおいて Scala でマイクロサービスを開発した経験に基づく持続可能なサービスの分割や結合にまつわる考察や知見をお話しさせていただきます。

内容について

以下に CfP に記載したトークの概要を引用します:

はてなにおける Scala でマイクロサービスを開発した事例についてご紹介します。

サーバー監視サービスの Mackerel (https://mackerel.io/) や現在進行中のはてなブックマークリニューアルで主たる言語として Scala を採用しているはてなで、新たなマイクロサービスの開発を Scala で進めました。

先行するプロジェクトの知見を活かして DDD (ドメイン駆動設計) を更に洗練し実践するために、ユースケース駆動設計を導入し漸進的な分析や、ビルドプロジェクト構成やプログラミング要素技術に関して様々な工夫をしています。

過去のはてなにおける統合パターンの歴史を踏まえつつ、なぜそう至ったのかという背景まで含めた持続可能なマイクロサービスの設計についてご紹介します。


はてなは今年で創業15周年を迎えました。成長と拡大に伴い、現在稼動しているサービスは内部向けのものを含め多数・多岐に渡っています。

完全に独立し単独で役目を果たすサービスは稀で、他のサービスと連携して役目を果たすことがほとんどです。サービス同士を結合する手法もその時々で模索し試行錯誤を重ねてきました。

現在、はてなでは結合のパターンとしてマイクロサービスを HTTP で繋ぐ手法が増えつつあります。 そこに至る背景や過去の結合パターンを振り返りますので、これからマイクロサービス化を検討している方や既にマイクロサービス化し運用されている方にとって、興味深い内容となるでしょう。

また Perl を主たる言語として採用してきたはてながなぜ Scala を選んだかについても触れますので、いわゆる LL を採用してきたが Scala を検討している方にも言語選択の実例として有意義ではないかと思います。

会場でお会いしましょう

既に参加登録が始まって おります。ぜひ参加いただき足を運んで聞いてみてください。会場でお会いできることを楽しみにしています!

「Hatena Engineer Seminar #6 〜インフラ編〜 @ Tokyo」を8/31(水)に開催します! #hatenatech

こんにちは、ウェブオペレーションエンジニアの id:y_uuki です。

約1年ぶりにHatena Engineer Seminarを開催します。今回は、インフラ編ということで、はてなのウェブオペレーションエンジニアを中心としたスタッフが、はてなのサービスを支えるインフラ技術に関する取り組みをご紹介します。

「アクセスログ解析」「サーバプロビジョニング」「MySQL運用」といったインフラ技術そのものの発表に加えて、「リモートワーク」「ウェブアプリケーションエンジニアからみたウェブオペレーションエンジニア」「セールスエンジニア」といった働き方や他の職種と絡めたLTもご用意しています。

冒頭では新CTO id:motemen からの挨拶もあります!

大規模なウェブサービスのインフラ技術に興味のある方や、はてなのウェブオペレーションエンジニアが普段何をやっているのかを知りたい方はぜひご参加ください。

セミナー後は、ささやかですが懇親会も予定しています! みなさまのご参加をお待ちしています。

イベント日程と会場

  • イベント名: Hatena Engineer Seminar #6 〜インフラ編〜 @ Tokyo
  • 日時: 8/31(水) 19:20-21:40(19:00 受付開始/懇親会含む)
  • 参加費: 無料
  • 定員:60名(応募者多数の場合は抽選とさせていただきます)
  • 会場:株式会社はてな 東京オフィス

タイムテーブル

※ 内容を変更する可能性があります

時刻 名前 タイトル 時間
19:00 - 受付開始・開場 -
19:20 motemen 開会の挨拶 5分
19:25 wtatsuru はてなにおけるログ解析のこれまでとこれから 20分
19:45 hagihala はてなのサーバプロビジョニングの話(仮) 20分
20:05 ichirin2501 MySQL運用とらぶるすとーり〜^3 20分
20:25 - 休憩 5分
20:30 kga (LT) アプリケーションエンジニアからみたはてなのインフラの話 5分
20:35 dekokun (LT) 東京にいながら仕事のほとんどを京都のエンジニアと一緒にしている私のリモートワークの話 5分
20:40 a-know (LT) セールスエンジニアを支え そうな技術 5分
20:45 - 懇親会 軽食と飲み物を用意しています! -
21:40 - お開き -

お申し込み方法

以下のページからお申し込みください。

hatena.connpass.com

2016年ウェブオペレーションエンジニアの新卒研修

ウェブオペレーションエンジニアの id:y_uuki です。2016年度のウェブオペレーションエンジニアの新卒研修を紹介します。

今年はウェブオペレーションエンジニアとして2名(id:masayoshi id:taketo957)が新卒として入社しました。若手のインフラ系エンジニアが少ないと言われる昨今で、もともと7人のインフラチームに2人も新卒が加わることはなかなか珍しいのではないでしょうか。

今年の新卒エンジニアは 2016年度はてな新人エンジニア研修を行いました - Hatena Developer Blog のエントリで紹介した新人エンジニア研修の後に、チームに配属されました。通例であれば、チーム配属後はOJTという名目で即実戦投入されます。しかし、今回は、OJTの前段に2週間程度の研修期間を設けてみました。

研修の動機

ウェブオペレーションエンジニアは、一般的なコンピュータサイエンスやウェブシステムの知識に加えて、社内のインフラ基盤や各サービス固有の知識も広く求められます。前者については、書籍やウェブ上の資料で体系的に学ぶことが比較的容易です。しかし、後者については、まとまった資料がない、というのはよくある話です。したがって、具体的なタスクをこなして、ボトムアップに学んでいくことになります。

それはそれで悪くはないのですが、トップダウンに全体の構成をみる機会がないことが問題だと思っていました。具体的には、以下のようなものです。

  • 部分しか知らないと、上位レイヤと下位レイヤの両方を睨んだ最適な解決方法を発見できない
  • 部分しか知らないと、既存の解決方法があることにすら気づかない
  • やったことないタスクをやるときに、作業見積もりができない

これらをある程度解決するために、新卒向けに研修を導入することにしました。

研修の設計

問題を踏まえて、どのように研修を設計したらよいか考えました。チームの状況を鑑みて、丁寧に資料を作成し、講義をする暇は残念ながらありませんでした。

そこで、自律学習とフィードバックをベースにした研修内容を考えました。課題を与えて、課題に対してアウトプットしてもらい、アウトプットを先輩社員がこまめにフィードバックするというものです。これで、業務に必要な知識がすべて身につくわけではもちろんないので、ポインタを知っておくことが大事です。

課題は知識課題、設計課題、実践課題の3種類用意しました。

知識課題

知識課題は、はてなのインフラの現状を理解するための課題です。はてなのインフラを構成する各トピックについて、自力で調査し、その結果をまとめてもらいます。1日ごとにトピックを用意し、次の日にアウトプット内容をみて先輩社員がフィードバックします。各トピックについて、一般的な知識はある程度あるものとして、はてなではどのようにやっているのかを把握することに主眼を置いています。

調査を進めるにあたって、社内のwikiをみてもよいし、GHEにあるリポジトリも自由にみてもらってよいし、サーバにログインして非破壊的なコマンドを実行してもよいとしました。

あまり長々と研修期間を設けるわけにはいかなかったので、かなり詰め込んだ内容になっています。

  • 1日目: サーバプロビジョニング
    • オンプレミス環境におけるブートストラップの仕組み: KickstartやXen環境の作成の仕組み
    • AWS環境におけるEC2インスタンス作成の仕組み
    • Chef運用
    • アプリケーションデプロイ (Capistrano)
  • 2日目: 仮想化基盤の構成
    • Xenの構成
    • LVMの構成
  • 2日目: ロードバランサの構成
    • LVS/TUN
    • LVSの台数やLVSとサブネットの関係など
  • 3日目: 冗長化の仕組み
    • Keepalived
    • MHA & ENI
  • 3日目: MySQL運用
    • フェイルオーバの仕組み
    • 参照用スレーブ
    • バックアップ方法
  • 4日目: サーバモニタリング
    • Mackerelのステータス管理やロール管理
    • Mackerelと各ツールの連携
  • 4日目: 典型的なサービス構成
    • オンプレミス: 人力検索はてなの構成
    • AWS: はてなブログの構成
  • 5日目: その他
    • 各種サービス探訪
    • 各リポジトリ探訪
    • ネットワーク構成

設計課題

設計課題は、スケーラビリティのあるシステムをどのように設計するのかを理解するための課題です。3日ほどで課題に対する設計案を考えてもらって、発表形式で議論しました。

設計課題を設けた理由は、最近のシステムが昔に比べて要求が厳しくなっているため、気軽に任せられるようなシステムが減ってきたことです。そこで、既存のシステムをベースに大きな機能を追加するときや、アクセスが10倍になっても耐えられるようにするにはどう設計するかを考えてフィードバックする機会があればよいと考えました。

今回のお題は、「Mackerelにおいて現状の100倍の稼働エージェント数に耐えられるシステムを設計せよ」でした。他の既存サービスや架空サービスをテーマにすることも考えましたが、一番難しくておもしろいやつがよかろうということで、Mackerelにしました。

いきなり100倍と言われても、とっかかりがないと考えにくいため、以下の手順で考えてもらうようにしました。

  • 1. 現状の構成
  • 2. 現状構成の限界エージェント数
  • 3. エージェント数 10倍にしたときの構成
  • 4. エージェント数 100倍にしたときの構成

アウトプットとして以下の要求をだしました。

  • システムの構成図
  • それぞれのロールにどれくらいの規模のサーバが何台くらい必要なのか
  • 現構成から新構成にするにあたってデータ移行をどのように行うか
  • 必要に応じてアプリケーションをどのように変更するか

正解はもちろんないので、アウトプットとアウトプットに対するフィードバックを通じて、システム設計の勘所が分かれば目的達成です。

実践課題

実践課題は従来からやっていたOJTと同じものです。基本的なオペレーションを含むメンテナンスタスクと本番サービスのシステム構築が主な内容です。

研修のKPT

設計研修が終わった段階で、研修を受けたメンティー2人とメンターでKPT会をしました。

メンティー

  • Keep
    • 研修を通じて横で一緒に作業してる時にそれどうやってるのみたいなのを共有できたりできたのは最高だった
    • 設計研修は少し重かったけど実際のサービスについて調べまくるのでPWGとかに出ても話の流れについていけるのが良かった
    • 研修で色々調べる事でどの情報が古いかとかを判断する機会になるのは良い事だと思った
    • 二人で相談しながら出来たので良かった
    • サービスの構成や、はてなインフラの構成の概要がわかった
    • git grep, ag でリポジトリ内を検索や、Mackerlを使った検索などもわかった
    • 役割分担をできてよかった
  • Problem/Try
    • 調べることに熱中して、メモや、時間などを気にしなかった場面があった
    • 調べる内容も本質ではないところを調べすぎたところがある
    • 資料作りに向けての時間をもう少し作ったほうが良かった
    • (設計研修では)他社の事例をもっと見ても良かった
    • 定量的に見られるようになりたい
    • もう少し質問をすべきだったのかも知れない
    • もう少しコードを見たほうが良い
    • 設計研修はもう少し時間が欲しかったかもしれない
    • サービス影響を考えて踏みとどまる事が多かった

メンター

  • Keep
    • 新卒同士で組んで課題に取り組んでもらうのは良かった。ペアで知識を補完しあってたのがよかった
    • 知識 => 設計の課題の流れがよかった
    • 設計課題が難しすぎるかと思ったけど、ちゃんとできてた
    • メンターの負荷を最小にしつつ、今後必要となる要素(はてなのシステムの全体把握)をある程度伝えられた気がする
    • 設計課題の成果を他のチーム(Mackerelチーム)のエンジニアにもみてもらえた
  • Problem
    • 知識課題でも毎日メンターからのFBは設けたが、うまく機能していない気がする
    • メンターから課題進捗が把握しづらくて、翌日の朝にレポートを見てその場でFBだったのでやりづらかった。仮にあまり芳しくない状況のときのリカバリが難しいかもしれない
    • 急いで作ったこともあって、全体的に課題の出し方が雑だった
    • 2人じゃないとしんどい課題もありそうだったので、次の入社が1人のときにどうするか
  • Try
    • 知識課題は具体的なトピックを充実させたほうが良さそう
    • 昼も雑談とかで軽く話したほうがいいかもしれない
    • 知識課題は質問をいくつか並べてそれに答える形がよさそう
    • 設計課題は成果物のイメージをちゃんと伝えられるように

あとがき

初の試みであるウェブオペレーションエンジニアの新卒研修を紹介しました。

もともとウェブオペレーションエンジニア向けにOJT以外の研修をやるという話はありませんでした。しかし、チーム配属直前に、以前自分が雑につくったOJT用issueが、今年の新卒向けに雑にコピペされていくのをみて、このままではまずいと思い、研修を計画しました。

はてな・ペパボ技術大会〜インフラ技術基盤〜@京都 行ってきたメモ - haya14busa にメモしてもらっていますが、 はてな・ペパボ技術大会の若手座談会で、おもしろかったと言及してもらえたので、やってよかったのかなと思いました。

はてなでは、ウェブオペレーションエンジニア職の応募をお待ちしています。ただいま絶賛募集中です。

YAPC::Asia Hachioji 2016 にはてなのエンジニア4人が登壇しました!

こんにちは。はてなの id:stefafafan です。
先日 YAP(achimon)C::Asia Hachioji 2016 mid in Shinagawa が開催されましたね。こちらのイベントに私も含め、はてなから4人のエンジニアが登壇しました!この記事ではそれぞれ発表した内容を簡単に紹介いたします。

はてなブログのAMP対応で学ぶウェブサービスのAMP対応

id:hitode909 によるウェブサービスのAMP対応についての発表です。AMPの紹介をはじめ、素朴なウェブサービスのAMP対応、そしてはてなブログのAMP対応についてお話しました。

発表資料

blog.sushi.money

ウェブサービスの開発フローケーススタディ

id:shimobayashi による開発フローについての発表です。チームでウェブサービスを開発するにあたってどのように開発フローを変化させていったかをケーススタディ形式でお話しました。

発表資料

speakerdeck.com

アプリケーションエンジニアのための監視入門

id:Songmu による発表です。Mackerelの紹介をしつつ、アプリケーションエンジニアのためのWebサービスの監視についてお話しました。こちらは「飛び込み枠」での発表でした。

Anime that I Recommend for Engineers to Watch

id:stefafafan によるエンジニア向けのアニメと技術でアニメファンをサポートする方法についてのLTでした。

発表資料

speakerdeck.com


いかがでしたか。今回こちらのイベントに参加できなかった方も、今後関東や関西のイベントではてなのエンジニアと直接お話する機会はありますのでご期待ください。

またはてなではWebアプリケーションエンジニア、Webオペレーションエンジニアともに募集しておりますので興味のある方は応募お待ちしております。
hatenacorp.jp

【7月4日(月)正午まで】はてなサマーインターン2016締め切り間近!!!

はてなでのひと夏があなたをつくります

下記の記事などでお伝えしております、今年のはてなサマーインターンの応募締め切りが迫ってきました。応募は7月4日(月)正午までとなっていますので、検討中の方はお早めに申し込みください。

developer.hatenastaff.com

developer.hatenastaff.com

今年はアマゾン ウェブ サービス ジャパン株式会社様より講師を招いての特別講義や、機械学習演習など、前半の講義パートも去年以上に充実しています。また、4つのコースから選択いただく後半課程も、はてなのスタッフと実際のサービス開発を体験できる密度の濃いカリキュラムです。

はてなで過ごす夏の20日間。皆さんとお会いできるのを楽しみにしています!!

募集概要

開催日程

2016年8月15日(月)〜9月9日(金)平日のみ20日間
(期間中フル参加できない場合の個別の日程調整は可能です。ご相談ください)

開催会場

株式会社はてな 京都本社(京都市中京区)

実施内容

1〜10日目: Webサービス開発/コンピュータサイエンス 講義・課題
11〜20日目: サービス開発/研究(コースごとに実施)

応募資格

2017年以降に卒業予定の高専生、大学生、ならびに大学院生
当てはまらない場合は応相談

待遇

日給 10,000円 (11日〜20日目の10日分を支給)
オフィスから片道1時間以上要する遠方からのご参加の場合
期間中ホテルを当社負担で手配(朝食代含む)
京都までの往復交通費として最大25,000円まで支給

募集人数

最大8名
※選考状況によっては一部コースが開催されないことがあります

選考

書類(場合により対面またはSkypeにて面接選考を行います)
選考結果のご連絡は7月中旬ごろを予定しています。

昨年のインターンの様子は、下記のレポートサイトをご覧ください。

TypeScript の型定義ファイルと仲良くなろう

はじめに

こんにちはアプリケーションエンジニアの id:t_kyt です。

このエントリでは TypeScript (以下 TS)における型システムの概要に触れつつ、型定義ファイルに何が書かれているかを理解するのに必要な知識を解説していきます。ある程度TSを書いたことあるが、なんとなく .d.ts ファイルをダウンロードしてきて使っている人向けです。

TypeScriptの型システム

型定義ファイルの書き方読み方に入る前にまず TS の型のシステムについて軽く触れておきます。

この章を読まずとも定型的に型定義ファイルの読み方書き方を覚えることはできますが、何が起こっているのかわからないと応用が効かないと思うので一読しておくことをおすすめします。

Declaration space

TSには declaration space という概念が存在します。同一の declaration space 上で同じ名前の entity (宣言した変数や型など)があった場合、コンパイルエラーとなります。ただし宣言が open-ended な場合や特別に振る舞いが定義されている場合はその限りではありません。 open-ended に関しては後ほど解説します。

declaration space には3種類あり、それぞれ ValueTypeNamespace です。

var X: string;    // Value named X

type X = number;  // Type named X

namespace X {     // Namespace named X  
    type Y = string;  
}

結果

このコードはすべて同一の X という名前で宣言されていますが、 declaration space が異なっているため、コンパイルエラーにならずそれぞれ有効です。

同一空間ではコンパイルエラーになるので

type X = number;
type X = number;

結果

のようにするとエラーになります。

var X: string;    
var X: string;

こちらの場合も、Valueに関して同一の declaration space に同じ識別子で宣言していることになりますが、振る舞いが特別に定義されているためエラーにはなりません。

Multiple declarations for the same variable name in the same declaration space are permitted, provided that each declaration associates the same type with the variable.

5.2.1 Simple Variable Declarations より

また namespace についても同一の declaration space に同一識別子で宣言してもコンパイルエラーになりません。 namespace の場合は後述する open-ended の仕様によるため例外的扱いになります。

(例外的なものも多いですが、原則的には同じ declaration space には同じ識別子で宣言はできません。)

ある宣言がされた時、どの declaration space に宣言されるかは Declaration Type によって変わります。

Declaration Type Namespace Type Value
Namespace X X
Class X X
Enum X X
Interface X
Type Alias X
Function X
Variable X

Declaration Mergingより。

Declaration Type は宣言の種類です。チェックマークはその各宣言がどの declaration space に属するかを表しています。

このうち実際の JS のコードになるのは Value と Namespace です。Type は生成された JS のファイルには影響せず、TS の世界でしか影響がありません。 Namespace の場合は Namespace 内に Value がないと何も生成されません。

参考: 10.1 Namespace Declarations

Open-ended

open-ended とは同じ装飾名の宣言があった時、自動的にマージされる性質のことです。

open-ended な宣言として有名なのは interface 宣言でしょう。

interface Document {  
    createElement(tagName: any): Element;  
}

interface Document {  
    createElement(tagName: string): HTMLElement;  
}

interface Document {  
    createElement(tagName: "div"): HTMLDivElement;   
    createElement(tagName: "span"): HTMLSpanElement;  
    createElement(tagName: "canvas"): HTMLCanvasElement;  
}

このコードはマージされるので

interface Document {  
    createElement(tagName: "div"): HTMLDivElement;   
    createElement(tagName: "span"): HTMLSpanElement;  
    createElement(tagName: "canvas"): HTMLCanvasElement;  
    createElement(tagName: string): HTMLElement;  
    createElement(tagName: any): Element;  
}

とした場合と同等になります。 namespace 宣言も open-ended であるため、前述した例外のような振る舞いとなります。つまり

namespace X {
    type X = string;  
}

namespace X {
    type Y = string;  
}

はエラーとなりません。

さて、 open-ended な宣言がマージされる条件に root container が共通であるということがあります。まずは container の概念について説明します。

ある entity が宣言された時、その container も自動的に決定します。具体的には

namespace N {
    let x = 1;  
}

ここで宣言された x の container は namespace N となります。

また TS では Top-Level に importexport があるファイルが module とされ、 module内の宣言では container はその module になります。例としては以下のようなものです。

export {}
var x = 1;

この場合 x の container はこの module になります。

また、 module 自体の container は global namespace になります。

まとめると

  • ある namespace 内で宣言された entity の container はその namespace
  • ある module 内で宣言された entity の container はその module
  • global namespace 内で宣言された entity の container は global namespace
  • module 自体の container は global namespace

です。

次に root container ですが、root container は宣言が export されているかどうかで変わります。

  • export されていない entity の場合 root container はその entity の container
  • export されている entity の場合の root container はその entity の container の root container

直感的には一番外側の module や namespace が root container ということですが、例を見ないと意味がわからないでしょう。

// A.ts
namespace outer {  
    var local = 1;           // export されていない
    export var a = local;    // outer.a  
    export namespace inner {  
        export var x = 10;   // outer.inner.x  
    }  
}

// B.ts
namespace outer {  
    var local = 2;           // export されていない
    export var b = local;    // outer.b  
    export namespace inner {  
        export var y = 20;   // outer.inner.y  
    }  
}

Top-Level に namespace で宣言した場合、その container は global namespace になります。したがって、2つの outer はどちらも global namespace が root container ということになります。

open-ended のルールが適用されるのは root container が共通で、宣言が同じ declaration space で行われている場合ですから、 outer は条件を満たし定義はmergeされます。

また outer 内で宣言されている innerexport されているので root container は global namespace となり open-ended の性質が適用されます。したがってこの outer の instance type は

{  
    a: number;  
    b: number;  
    inner: {  
        x: number;  
        y: number;  
    };  
}

と同等になります。 export されていない宣言( local )については root container が共通でないためマージされません。(そもそも export していないので外からは見えません。)

ここまでの確認

いろいろ書きましたが、これで TS を書いている時「なんで?」となる(型定義に関する)ハマりポイントが仕様的にどう解釈されているかある程度理解できるようになったかと思います。

これまでの知識の確認をするため、ありがちなコードを例に少し今までの知識を振り返ります。

以下のコードは上手く行かない例です。

import $ from "jquery";

// interface をマージして window.originalFunction() を使えるようにしたい
interface Window {
    originalFunction(string): void
}

window.originalFunction("test"); // => 使えない…

window: Window; // type annotationしてみる

window.originalFunction("test"); // => 使えるようになった!!
window.alert('hello,world');     // => 元から Window あった定義が使えなくなった……

このコードで何が起きているかというと、 Window は新しく宣言されただけでマージされていません。type annotation した時点で window (変数)の方が既存の Window (interface)から新しく定義された Window に書き換わっています。なので window の型は既存の定義か、新しい定義のどちらかになってしまうのです。

では何故マージされないのか?についてですが 既存の Window と新しく定義した Window の root container が異なっているためです。既存の Window の root container は global namespace ですが、上記のコードで宣言された Window の root container は import $ from "jquery"; が存在するため、この module になります。 Top-Level に import を書いた場合 module になること、 export されていない entity の root container の決定の仕方を思い出してください。

ではどうすればいいのかについてですが、 root container が共通になるように宣言すれば良いのです。具体的には別ファイル( module になっていない = importexport が書かれていない)に宣言を移すか、後述する declare global { } を使うといいでしょう。

次に TS では Top-Level に importexport があると module として扱われる確認です。

// x.ts
namespace N {
    export x(): void;
}
// y.ts
namespace N {
    export y(): void;
}

この場合 module にはなっていないので使う側は特に import する必要はなく

N.x()
N.y()

のように即使用できます。使う側も module にはなっていません。また N の定義も統合されるので N 以下に xy が存在する形になります。

対して以下のような場合は module として扱われるので namespace の場合とは違います。

// x.ts
export namespace N {
    export x(): void;
}
// y.ts
export namespace N {
    export y(): void;
}

使う側としては

import {N as N1} from "./x.ts";
import {N as N2} from "./y.ts";

N1.x()
N2.y()

のように使います。使う側も import があるため module として扱われます。

そのファイルがいま module なのか?を意識すると定義ファイルでハマることも少なくなると思います。

型定義ファイルを読み書きできるようになるために

さてここからは、declare の説明をしたあと、型定義ファイルを読み書きできるように典型的な例を紹介していきます。

何が起こっているのかについては前章の知識で大体カバーできるかとおもいます。ただ、型定義ファイルの書き方に関するベストプラクティスは歴史的な経緯が入る部分もあるため深入りはしません。幾つかの典型的な例を示しつつ、標準的な型定義ファイルに対して何をやっているのか理解でき、標準的な書き方ならばできる、また標準的ではない型定義ファイルに対しても「何故」はともかく「何をやっているか」は理解できるようになることを目指します。

declare キーワード

declare キーワードを使うと既存の JavaScript の型情報が表現できます。

例えばグローバルで宣言されたtest()という関数を使うというシチュエーションを考えた時、以下コードは JS としては問題ない(本当にグローバルに test() があれば)ですが、tsc でコンパイルしようと思うとエラーになります。

test()
test.ts(1,1): error TS2304: Cannot find name 'test'.

これはどういうことかというと TS の declaration space に test が宣言されていないということです。ではどうすればいいかというと、ないのであれば宣言すれば良いのです。そんなときに declare キーワードを使います。

declare function test()

declare を使うと JS のコードとして出力されません。これにより既存の test() のコードを上書きすることなく test() が宣言することができますので、ブラウザや既存の JS によって提供される変数や関数を宣言する事ができます。これが ambient (包囲した、取り巻く)宣言です。

宣言すれば、以下のコードはコンパイルが通ります。

declare function test()
test();

みれば分かるように declare 自体は特に type annotations は必須ではありません( --noImplicitAny が有効になっていない場合。有効な場合明示的に any が必要です )。ただ declare は型情報もかけるので書いたほうが便利ということで普通は書きます。ambient 宣言を集めたファイル( .d.ts )は型定義ファイルなどと呼ばれて利用されています。

// x.d.ts
declare function test(): string;

// y.ts
let t = test(); // t は型推論で string になる

.d.ts ファイルはコンパイルした結果、JS のコードは生成されません。というよりは、生成しない宣言しかしてはいけません。

既存のオブジェクトの型定義を拡張する

ここからは今までの知識を利用して実際に定義ファイルを読み書きできるようになるための実例を示していきます。

実行環境がブラウザの場合 window オブジェクトが存在していますが、その型定義を拡張したい場合を考えます。

自分で拡張せずとも標準的なものは Window として typescript/lib/lib.dom.d.ts に定義されていますが、今回は例えば window.fetch() など polyfill した場合や自分で独自のプロパティを生やしているシチュエーションを想定しています。

そういった場合は既に定義されている Window に対して新しい定義をマージすることで対処します。

interface Window {
    fetch(url: string|Request, init?: RequestInit): Promise<Response>;
}

これは window.fetch の例ですが RequestInit などの定義は省略します。これで既に TS が標準で定義している Windowfetch がマージされます。

グローバルなオブジェクトに対する宣言

例えば jQuery を script タグで読み込んだ時のようなグローバルにオブジェクトが存在しているような場合を考えます。その場合

declare namespace $ {
    function x(): void;
    var y: string;
}

のように namespace で対処すると良いでしょう。これにより $.x()$.y みたいな呼び出しが可能になります。なお declare namespace 内では定義された entity はデフォルトで ambient 宣言になるため declare は必要ないです。

少し混乱すると思いますが、実際の DefinitelyTyped で公開されている jquery.d.ts はこの方法では書かれていません。これは歴史的経緯や他の定義ファイルとの依存関係の話になるので、特に気にしないで namespace を使うといいでしょう。

もちろん単なる関数オブジェクトの場合は

declare function f(): void;

のようにするだけで十分です。

ここで1つ今ある定義ファイルを読みやすくするポイントして、 namespace が導入されたのは結構最近なので、以前からある定義ファイルでは module となっている場合があるということを頭に入れておくといいでしょう。ES6のモジュールシステム導入にともなって

  • internal module( module 'N' {} ) → namespace( namespace 'N' {} )
  • external module( module M {} ) → (単に)module( module M {} )

というように用語とキーワードが改められました。意味は同じですが module(旧 external module)と紛らわしいので namespace が推奨されています。

module

npm module を使いたいとき、単に npm i lodash しただけでは TS の世界では import できません。

import * as _ from "lodash";

module の場合は別ファイルにそれ用の宣言が必要になります。

declare module 'lodash' {}

module の後が文字列なのがポイントです。識別子だと namespace になります。

ただし、これだと lodash という module があるという情報しかないので定義を追加していきます。

declare module 'lodash' {
    interface Hoge{}
}

にすると

import * as _ from "lodash";
var hoge: _.Hoge;

がコンパイルを通るようになります。

module も open-ended なためマージすることができます。

declare module 'lodash' {
    interface I {
        x(): string
    }
}

という既存の .d.ts ファイルが存在しているが、自分で定義を追加したい場合は以下のようなファイルを用意すれば良いということです。

declare module 'lodash' {
    interface I {
        y(): string
    }
}

ただし、module が open-ended になったのは最近のことなので、namespace を使って無理やり拡張できるように定義されている場合もあるので頭に入れておくと混乱せずに済むでしょう。

参考: JS資産と型定義ファイル

Export Assignments

Export Assignments(export = )を使った例を見ていきます。

現在の TS では ES2015 形式のモジュールシステムが使えるため、Export Assignmentsで書く必要がない場合もあります。しかし、 export =export default に互換性がないこともあり、既存の定義ファイルの大多数は Export Assignments で書かれていると思うので、むしろ ES2015形式 より多く見かけると思います。

参考: ES6 Modules default exports interop with CommonJS

import sayHello = require("say-hello");
sayHello("Travis");

このように単体のオブジェクトが export されている module の場合、定義ファイルは

declare module "say-hello" {
    function sayHello(name: string): void;
    export = sayHello;
}

のようになります。 export = は単体のオブジェクトを export できます。

import M = require("M");
// ES2015でいうと
// import * as M from "M";

のように利用する module 、つまり M というオブジェクト以下に複数の export されたオブジェクトがぶら下がっているような場合は namespace を Export Assignments で export します。

declare module "M" {

    namespace N {
        function x(name: string): void;
        class C {}
     }

     export = N;
}
import m = require("M");

m.x("Travis");
let C = new m.C();

interface も利用できます。

declare module "M" {

    interface I {
        (): I;
        new (): I;

        x: string;
        y(): void;
    }

    declare var i: I;

    export = i;
}

間違いやすいポイントとしては export = に何を指定するかです。 interface である Iexport = をしてしまうと、 インスタンスではなく型そのものが export されてしまうのでうまくいきません。type annotation された変数を export = しましょう。

Relative or Non-relative module imports

補足ですが、以上はすべて non-relative module の例です。つまり require()from で指定する部分を

import $ = require("jquery");

import * as $ from "jquery";

のように module 名でする場合です。npm の module を利用する場合などはこうする場合が多いと思います。

対して relative module import とは

import Entry from "./components/Entry";

のように相対パスで指定する場合です。自前の module はこちらでしょう。

既存の(自前の) JS を .d.ts ファイルとともに使用する場合は .js ファイルと .d.ts ファイルは同じディレクトリに入れておくのが普通だと思います。その場合、 from 以降で指定されている path が一致しているので、今までの例にある一番外側の declare module M は必要ないです。declare module M は non-relative module import における module 名を指定していると考えてください。

TS における module の解決の仕方も頭に入れておくと理解しやすいです。

参考: Module Resolution

ES2015形式

ES2015 形式の module でも記述することが出来ます。 declare キーワードが付いてても export が書けるので以下のようになります。

// m.ts
export declare function f(): void;
export declare class C {}

この場合 namespace でまとめてから export = みたいなことはせずに済みます。

import する側は

import * as m from "./m";

のように利用します。これは

import m = require("./m");

と同義です。

実際の定義ファイル

これで大体の型定義ファイルのパターンがカバーできたと思います。ここで一つ実際の例(jQuery)を見てみましょう。

declare module "jquery" {
    export = $;
}
declare var jQuery: JQueryStatic;
declare var $: JQueryStatic;

ここより上の部分はひたすら JQueryStatic の定義をしているだけなので割愛します。Top-Level に interface が宣言されているのであまり良いとは言えないのですが、型定義ファイルの書き方のベストプラクティスは事情によりけりなので今は触れないことにします。 参考: JS資産と型定義ファイル

declare var jQuery: JQueryStatic;
declare var $: JQueryStatic;

この部分で global に $ 及び jQuery という変数が存在すること、またその変数は JQueryStatic であるということが TS のコンパイラに伝わります。これで global な jQuery の読み込み(scriptタグで読み込んだ場合など)には対処できます。

しかし、JQuery を npm module として読み込みたい場合これでは不十分です。それを補うのが

declare module "jquery" {
    export = $;
}

この部分です。

この記述により jquery という module が存在することが TS のコンパイラに伝わります。この記述があって初めて

import * as $ from 'jquery'

という module 名の読み込みが可能となります。なぜ interface を直接 export しないのかについては前述のとおりです。

これでめでたく script タグで読み込んだ場合と npm module として扱いたい場合の対応ができました。

既存の定義ファイルを拡張する

基本的には既存のオブジェクトの型定義を拡張するで触れたように open-ended な宣言に対するマージで対処します。 interface だけでなく module( declare module 'M' {} )や namespace( declare namespace N {} )も open-ended であるため、マージされる条件が整えば既存の型定義ファイルを拡張することが出来ます。

ただし export = が使われていた場合注意が必要です。

declare module 'M' {
    namespace N {
        var x:string;
    }
    export = N;
}

このようになっている定義ファイルは外から拡張する方法はありません。なので定義ファイル自体書換える等の処置が必要になります。

ES2016 形式で書かれている場合は外から拡張可能です。

declare module 'M' {
    export var x: string;
}

に対しては別ファイルに

declare module 'M' {
    export var y: string;
}

と書けば定義は拡張されます。

export = が使われていた場合、外部から拡張する術がないのは微妙に困ることが多いですが、TS 2.0 (次期バージョン)以降では以下のような書き方ができます。

import * as M from 'M';

declare module 'M' {
    function x(): void;
}

M.x();

参考: Unable to augment export = function/namespace

ちなみに TS の nightly builds は npm install typescript@next でインストールできます(現行は Version 1.9.0-dev.20160611-1.0 ですが上記の書き方はできるようになっています)。

declare global { } について

TS 1.8 より declare global { }導入されました。この記法により module 内でも global namespace に型を定義を宣言できます。

import $ from "jquery";

// interface をマージして window.originalFunction() を使えるようにしたい
declare global {
    interface Window {
        originalFunction(string): void
    }
}

window.originalFunction("test"); // => 使える
window.alert('hello,world');     // => 使える

先ほど上手く行かない例としてあげたコードもこのように書けばファイルを分割することなく Window を拡張できるようになります。

Typings について

最近、長らく TS の型定義ファイル管理ツールであった TSD が非推奨になり、Typingsが登場しました。

TSD にはいくつが問題点があったのですが、特に型定義ファイルの global な読み込みについてこのエントリと関連深いため触れておきます。

前述の jQuery の例を見るとわかるのですが、 jQuery を module として読み込んだ場合でも $JQueryStatic が global に宣言されてしまいます。 declare が付いているので declaration space は Type となり、実際の成果物である JS には影響を及ぼさないため害がないといえばないですがあまりいいものではありません。たとえば jQuery を import していない場合でも以下のコードは問題なくコンパイルされます。

 $('.xxx').hide();

しかし実際の JS は module 化されているので $ オブジェクトは存在せず、実行時エラーとなります。 これを解決するには import しているファイルでのみ定義ファイルを referenceタグ( /// <reference path="path/jquery.d.ts"> )を使って読み込む等の地道な作業が必要でした。

Typings はこの問題を解決しています。Typingsが提唱する形式の型定義ファイルは module として読み込むか、global に読み込むかを選択できます。module として読み込む場合 Typings 側で自動で global に散らばった定義を隠蔽してくれます。したがって、script タグで読み込んだ場合でも npm module として読み込んだ場合でも両方適切に対処できるようになります。

ただし、 module 化した場合、外から拡張する手段が現状(TS 1.8)では存在しないので定義ファイル自体を書き換えるしかありません。先ほど触れた 1.8.2 での変更が入れば外部からの拡張が可能です。

Typings の形式に対応した型定義ファイルはまだ多くないのですが、オプションを付けることで DT のファイルも使うことができます( module 化はしてくれないので tds で読み込むのと同じになる)。

ちなみに TS2.0 からは npm で型定義ファイルを管理できるようになるため Typings も一時の繋ぎになるかもしれません。 参考:The Future of Declaration Files

おわりに

TS の躓きポイントの1つである型定義ファイルの読み方書き方について、型システムを仕様を少し掘り下げることで何が起こっているのかを解説しました。

TS の型定義ファイルで困ったらむやみに <any> する前に一度立ち止まり正しく型を付けれないかを考えてみましょう。

インターン募集中

hatenacorp.jp

応募締め切りは2016年7月4日(月)までです!奮ってご応募ください!!

ペパボ・はてな技術大会〜インフラ技術基盤〜の参加者募集中!!!


上記エントリにて先日お知らせしたペパボ・はてな技術大会〜インフラ技術基盤〜の募集を開始しています。すでにconpassのイベントページを公開していますが、本ブログでも改めてお伝えします。

以前にお伝えしたとおり、福岡と京都で開催します。福岡編は、7月9日(土)開催です。参加者募集ページは以下になります

京都編は、7月2日(土)開催です。参加者募集ページは以下になります。

福岡、京都ともにすでに定員を上回る申し込みをいただいています。ただし、先着順ではなく抽選で募集していますので、今からでもふるって申し込みください。