読者です 読者をやめる 読者になる 読者になる

角待ちは対空

発生19のガードされて+1を忘れるな

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

TypeScript

トラブルシューティング形式で TypeScript との関わり方を示していく。導入や書き始めのハードルを下げるのが目的なので意識高いことは言わない。

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

対象読者

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

社内向けに書いているので固有の事情は入る。例えばこれだけ any にキャストすればいいと書いたとしても本当にキャストしまくる人間はいない、という信頼の元書いてる。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 書けないかというと別にそんなことはない。今まで 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 の書き方をpattern分けするとプロパティシグニチャ、コールシグニチャ、… というように分類できる。 interface の書き方分からないと思ったら シグニチャ で調べると良い。大体は感覚で書ける(& 他のプロジェクト見れば良い)。

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

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

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

型が合わない時

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

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

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

キャスト色々

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

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

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

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 した場合自動で見に行ってくれるため読み込めないことはないと思う。それ以外の方法で定義ファイルを用意した場合、自分でパスを指定して読み込むこと。

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');

あたりを片っ端から試してみると良い。

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

developer.hatenastaff.com

あとは allowSyntheticDefaultImports オプションの存在も知っておくと完璧。とにかく型定義ファイルは慣れるまで見ない方針がおすすめ。

コンパイルオプション

{
    "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"; を消してしまった場合が怖い。 なので importHelpersimportHelpers がない場合エラーを出すようにするといい。

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 とか一年前にはなかった気がする。

オーバーロード

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

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

型関連システム

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

https://www.typescriptlang.org/docs/handbook/advanced-types.html

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

Union Types

type A = number | string | boolean;

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

numberstringboolean か判断できないのでこうなっているだけで、実際はどれなのか特定して型を狭めていきたいはず。その仕組を Type Guards と呼んでいる。

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

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 で。

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