角待ちは対空

おもむろガウェイン

私的TypeScriptとの関わり方ガイドライン

初めて書く時困りそうなトピックごとに TypeScript との関わり方を示していく。導入や書き始めのハードルを下げるのが目的なので意識高いことは言わない。

https://github.com/remojansen/logo.ts

対象読者

  • JS は書ける
  • そんなにやる気はないけど TS を書かなくてはいけなくなった
    • 全く知らないわけではない

社内向けに書いているので固有の事情は入る。

ゴール

  • babel 使うくらいなら TS 導入しようという気持ちになる
  • いきなり TS 書かなくちゃならなくなってもそんなに慌てず済む

基本姿勢

  • お前がどんな型を書こうが JS には一切影響なし
    • TS を書いているのか JS を書いているのか意識すること
  • TS は補助輪。補助輪なしで走れる( JS が書ける )なら補助輪の外して走っても良い
    • 補助輪錆びてたら外していい。外したことは分かるようにしてほしい(コンパイルオプション)
  • 最終的にはチームの方針に従って
    • any は許さないという方針であるならばチームでケアできるはずなので

何故そんなこといい加減な感じなのか

A: 流れが早すぎて仕様を把握しろと言うのは酷だから かつ 型関連でいい加減なことしてもコンパイル結果には影響しないから。

例えば最近入った変更。

  • type annotationが要らなくなった
let n;      // => noImplicitAny 下でもエラーにならない
n = 'test'; // => この時点で string 型
n = 1;      // => この時点で number 型
  • キャストが要らなくなった
// http://qiita.com/vvakame/items/305749d3d6dc6bf877c6#条件式によるnumber-or-stringからリテラル型への変換
function suite(v: "spades" | "diamonds" | "hearts" | "clubs") {
}

let a: string = "";
switch (a) {
    case "spades":
    case "diamonds":
    case "hearts":
    case "clubs":
        // a は "spades" | "diamonds" | "hearts" | "clubs"
        // 前は a は string のままだったのでコンパイルエラーだった
        suite(a);
}
  • String Literal Type の仕様変更
const a1 = "a";   // => これは "a" 型
let   a2 = "a";   // => これは string 型

let b1: "a" = a1; // => 通る
let b2: "a" = a2; // => 通らない

こういう変更がバンバン入ってくるのが TS なので普段書いてても意識して追わないと仕様がわからなくなる。

しかし、これらの変更を知らないと安全に TS 書けないかというと別にそんなことはない。今まで書いてたわけだし、なんなら JS でだって安全なソフトウェアは作れる。ただ今まで annotation や キャスト が必要だった場所にそれらがいらなくなるだけという話。

TS の膨大な仕様を覚えたくないから TS を書きたくない、というのはもったいないので「TS の仕様がわからないのであれば部分的に JS を書けばいいよ」というのがいいたいこと。部分的にサボったとしても TS を書くメリットはある。

型の書き方

type annotation

: string みたいな記法のやつ。noImplicitAnytrue にしておけばここに type annotation 書けって怒ってくれるのでそれに従って書けば良い。 prettyture ならば見やすい。関数の返り値は警告でないので意識して書くこと。最低限 voidany は書いてほしい。

変数宣言は型推論が効くので、メソッドや関数の引数と返り値が主な annotation を書かなくてはいけない箇所だと思う。

かつては空の配列は annotation しておいたほうが良い箇所だったけど今は推論される。

let r = []  // => この時点で any[]

r.push("")  // => この時点で string[]
r.push(1)   // => この時点で (string|number)[]

(string|number)[] になった時点で共通のプロパティにしかアクセス出来ないのでおかしなことになることはない。ただし、最初に let r: (string|number)[] = [] とか書いたほうが可読性が上がるみたいな話はありそう。

シグニチャ

interface の書き方をパターン分けするとプロパティシグニチャ、コールシグニチャ、… というように分類できる。 interface の書き方分からないと思ったら シグニチャ で調べると良い。大体は感覚で書ける(& 他のプロジェクト見れば良い)。

感覚で書けなさそうなのはインデックスシグニチャ

// http://www.buildinsider.net/language/quicktypescript/01
interface SampleD {
  [index: number]: boolean; // 添字にnumberを使いbooleanを格納できる
}

関数の返り値とかでオブジェクトの型を指定したい場合は interface を書くのではなく直で書けば良い。使いまわすのならば interface 書く。

型が合わない時

Structural typing

TS の型システムはこれに基づいている。端的にいうと同じプロパティ持ってて型が一緒ならば同じ型とみなす。

C# とか Java ならば以下は通らないけど TS なら通る。JS のことを考えると何故そうなっているのか自然に理解できるはず。

interface Named {
    name: string;
}

class Person {
    name: string;
}

let p: Named;
// OK, because of structural typing
p = new Person();

https://www.typescriptlang.org/docs/handbook/type-compatibility.html

この辺がわかっていればそんなに型のシステムで困ることはないはず。 常に JS の気持ちを考えてあげると大体うまくいく。

any したい

最近の TS は賢いのでキャストしたくなったら大体コードが間違っている。とはいえ必要なときが全くないとは言い切れないので、判断できないのであれば JS として正しいかを意識し ながら any にキャストして JSを書けばいい

また型定義ファイル自体がおかしい場合もあるので原因の切り分けが難しいこともある。 any って書いてチームに詳しい人がいるなら指摘してもらえばいいし、いないならそのままで良いと思う。

関数単位で引数と返り値の型が書かれていれば、内部的な処理が any だからけでもそんなに困らないと印象。部分的に JS になるだけ。

キャスト色々

let a = <any>someFunction();  // 基本
let b = (<any>obj).getProp(); // ドットアクセス時
let c = d as any;             // as で後置できる(TSX用)

キャストせざるを得ない時

let element = <HTMLImageElement>document.getElementById('img-id'); // => HTMLElement | null

みたいなケースが一番多いはず。

id と 要素の対応は TS が知る由もないので HTMLElement と定義するしかない。したがって HTMLImageElement の API が使いたければ、キャストが必要。同じ原理で event.target もキャストが必要。ただし、いつでもキャストする必要はなく HTML*Element 固有の API を使いたいときだけで良い。そうでないときは HTMLElement のままで良いのでキャストしなくていい。

(本来ならばDOM に存在しない場合があるのでまず ifnull チェックしたほうが丁寧だが、ここでは省く。)

import できない

noImplicitAnyfalse ならば型定義ファイルがなくても ./node_modules 内にインストールされていれば import できるようになったので何も考えなくて良い。

とはいいつつ noImplicitAnyfalse にするのはおすすめしないので解決のヒントを。

error TS2307:Cannot find module 'hoge'.

定義ファイル自体がないので置き場所が悪い。ちゃんと読み込める場所にあるか確認を。 traceResolutionture にすると探しに行く様子が見れるので役に立つかもしれない。

ちなみに現在定義ファイルの管理ツールのデファクトは npm で npm i @types/lodash みたいに使う。大抵のファイルは用意されているはず。 @types/hogenpm install した場合自動で見に行ってくれるため読み込めないことはないと思う。

更に言うならばパッケージ内に型定義ファイルが含まれていればそれを参照してくれる。自分の使いたいライブラリに d.ts が含まれていればそれを使えば( =なにもしなくて )良い。

それ以外の方法で定義ファイルを用意した場合、自分でパスを指定して読み込むこと。

error TS1192: Module '"hoge"' has no default export. や error TS2305: Module '"hoge"' has no exported member '_'. など

importの仕方が間違ってる。 /path/hoge を見て適切に import しよう。という感じだけど d.ts なんて読んでられないと思うので

import {_} from 'lodash';
import * as _ from 'lodash';
import _ from 'lodash';
import _ = require('lodash');

あたりを片っ端から試してみると良い。オブジェクトを export していないのならば import 'tslib'; みたいなパターンもある。

いやちゃんと理解したいという場合は commonjs 方式のmodules と es6 modules の記法をちゃんと理解した上で、手前味噌だが以下の記事読むと良い(2.0 以前なのでちょっと古いけど問題ないはず)。

developer.hatenastaff.com

あとは allowSyntheticDefaultImports オプションの存在も知っておくと完璧。

頑張って

TS の最大のハマりどころはモジュールの型定義ファイルがうまく読み込めないことだと思うので頑張って欲しい。

原因究明のコツとしては結局

  • コンパイルは通るか => 通らないのであれば型定義ファイル自体読み込めていなかったり、 import の仕方がいけなかったり
  • browserify した後にうまく動かない => 型定義と JS の実装がズレている

というように問題の切り分けをしていこうという一般的なことしかいえない。問題がわかったところでどう修正すればいいかは( 特に後者の場合 )結構難しい話なので、詳しい人に聞くのが一番だと思う。いない場合自分がいちばん詳しくなる覚悟で。

散々言ったけど、マイナーなライブラリでもない限りそんなに困ることはないという印象( =困っても解決策にすぐたどり着ける )。

コンパイルオプション

{
    "compilerOptions": {
        // 到達しえないコードがあるとエラー
        "allowUnreachableCode": false,
        // 到達しえないラベルがあるとエラー
        "allowUnusedLabels": false,
        // use strict; が自動でつく(module ならば勝手につくけど)
        "alwaysStrict": true,
        // 暗黙的な any があるとエラー
        "noImplicitAny": true,
        // 後述
        "noImplicitReturns": true,
        // this に型を明示しないとエラー
        "noImplicitThis": true,
        // 使っていないローカル変数があるとエラー
        "noUnusedLocals": true,
        // 使っていない引数があるとエラー
        "noUnusedParameters": true,
        // デフォルトで non-nullableになるので null チェック必須に
        "strictNullChecks": true,
        // エラーが可愛くなる
        "pretty": true,
        // 後述
        "noEmitHelpers": true,
        "importHelpers": true
    }
}

これが一番強い。特に noImplicitAnynoImplicitReturnsstrictNullChecks は新規プロジェクトでは絶対に切らないこと。移行プロジェクトならば仕方ないが早めに true を目指すと良い。

ちなみに tsconfig.json はコメントが書ける。

noUnusedParamete と仮引数

function f(x: number, _y: boolean) {
    console.log(x)
}

noUnusedParametetrue にすると使っていない引数があると怒られるのだが、 _ を頭に付けると怒られなくなる。

ということで _ は TS 界では区別な意味を持つので使わないほうが無難。

this への type annotation

function f(this:string) {
  this.toUpperCase(); // this の型がわからないと toUpperCase() があるかわからない
}

みたいな記法で this の型を指定できる。引数っぽくみえるけどコンパイルすると消える。キモい。noImplicitThisthis に触らなければ type annotation なくても怒られない。

noImplicitReturns

返り値の型を書かないと怒られる...ではなく、暗黙的に undefined 返してたら怒られるので返り値はしっかり書くように。

function f(x: number) {
    if (x < 0) return 42;
}
// => error TS7030: Not all code paths return a value.

noEmitHelpersimportHelpers

TSのヘルパー関数はファイルごとに生成されるため重複することになる。それを防ぐのが noEmitHelpers でヘルパ関数を生成しなくなる。生成しなくなるだけだと困るので npm i -save tslib して import "tslib"; をどこかに書けば重複させずにヘルパ関数を生成できる。

これだけだとうっかり import "tslib"; を消してしまった場合が怖い。 なので importHelpersimport "tslib"; がない場合エラーを出すようにするといい。

TS 特有のキーワード

type

型の alias を定義できる。

type a = string | boolean

個人的には関数スコープ内で型に名前作りたくなった時くらいにしか使わない。

interface との使い分けは公式ドキュメント参照。

TypeScript-Handbook/Advanced Types.md at master · microsoft/TypeScript-Handbook · GitHub

!

let n: null | number = null; // => null | number
n!.toString();                      // => number

nnull または number なので本来ならば toString() は使えない(TS の union type は全ての型に共通に存在するプロパティにのみアクセスできる)。が、! をつけると number にキャストされ toString() に呼び出すことができるようになる。

実際 nnull なので n!.toString() したらエラーになるのでやめよう。治安を維持したいなら

if (n != null) {
    n.toString();
}

みたいに書くべき。! は所詮キャストなので実際の JS の型とずれることになる。

declara

型定義書くときに必要。片手間で書くのであれば知らなくて良い。

namespace

昔の JS のグローバル汚染防ぐためにオブジェクトに色々生やしてたあれができるキーワード。 対になる概念は moduleimportexport が書いてあったらそれは module である。今時 namespace 使うことはほぼ無いと思う。

abstract とか readonly とか private とか

JS にはないわけだけどまぁオブジェクト指向言語にはある一般的な概念なのでそんなに困らないと思う。もちろん TS の仕様は把握する必要あり。

仕様追加の速度が早いのでいつの間にかできるようになっていたりする。 プロパティへの abstract とか readonly とか一年前にはなかった気がする。

keyof in

blog.yux3.net

オーバーロード

あんまり便利じゃないので自分で書く時使うことは思う。

  • 型ごとに実装を持てない
    • 所詮は JS なので
  • 順番とか気にしないといけない

型関連の仕様

すごい勢いで追加されていくので説明している余裕はなし。全部知りたいなら公式ドキュメント見ましょう。

使用頻度が高いやつの紹介だけ。

Union Types

type A = number | string | boolean;

みたいなやつ。 A 型は number string boolean 共通のプロパティにしかアクセスできなくなる。

numberstringboolean か判断できないのでこうなっているだけで、実際はどれなのか特定して型を狭めていきたいはず。その仕組みを Type Guards と呼んでいる。大層な名前をしているけど普通にJSを書けばいい感じに判定してくれるという話なだけ。当時は制御構文見て決定しててすげぇ!という感じだったが最近は普通に思えてきた。

やり方はドキュメント見てほしい。

TypeScript-Handbook/Advanced Types.md at master · microsoft/TypeScript-Handbook · GitHub

ただ User-Defined Type Guards は

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

みたいな記法で知らないと返り値みてぎょっとする。

String Literal Types

string 型より一歩進んで値まで指定できる。かなり便利。

type Easing = "ease-in" | "ease-out" | "ease-in-out";

Easing 型に ease-in ease-out ease-in-out 以外の値を入れようとするとエラー。

所詮文字列なのでuglifyしてもそのまま残る。残したくない場合は enum で。

JS プロジェクトを TS に移行する

僕自身大規模なプロジェクトを移行させたことないので実際どうなるかは分からないが機能面のサポートとしては

があるので、 babel 使ってたというプロジェクトならばなにも考えず babel 部分を tsc に置き換えればいけるはず。

その後は徐々に .ts に書き換え、型を付けていくとよい。

読むと良いドキュメントなど