初めて書く時困りそうなトピックごとに TypeScript との関わり方を示していく。導入や書き始めのハードルを下げるのが目的なので意識高いことは言わない。
https://github.com/remojansen/logo.ts
- 対象読者
- ゴール
- 基本姿勢
- 型の書き方
- 型が合わない時
- キャストせざるを得ない時
- import できない
- コンパイルオプション
- TS 特有のキーワード
- オーバーロード
- 型関連の仕様
- JS プロジェクトを 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
みたいな記法のやつ。noImplicitAny
を true
にしておけばここに type annotation 書けって怒ってくれるのでそれに従って書けば良い。 pretty
が ture
ならば見やすい。関数の返り値は警告でないので意識して書くこと。最低限 void
か any
は書いてほしい。
変数宣言は型推論が効くので、メソッドや関数の引数と返り値が主な 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 に存在しない場合があるのでまず if
で null
チェックしたほうが丁寧だが、ここでは省く。)
import
できない
noImplicitAny
が false
ならば型定義ファイルがなくても ./node_modules
内にインストールされていれば import
できるようになったので何も考えなくて良い。
とはいいつつ noImplicitAny
を false
にするのはおすすめしないので解決のヒントを。
error TS2307:Cannot find module 'hoge'.
定義ファイル自体がないので置き場所が悪い。ちゃんと読み込める場所にあるか確認を。 traceResolution
を ture
にすると探しに行く様子が見れるので役に立つかもしれない。
ちなみに現在定義ファイルの管理ツールのデファクトは npm で npm i @types/lodash
みたいに使う。大抵のファイルは用意されているはず。 @types/hoge
を npm 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 以前なのでちょっと古いけど問題ないはず)。
あとは 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 } }
これが一番強い。特に noImplicitAny
と noImplicitReturns
と strictNullChecks
は新規プロジェクトでは絶対に切らないこと。移行プロジェクトならば仕方ないが早めに true
を目指すと良い。
ちなみに tsconfig.json
はコメントが書ける。
noUnusedParamete
と仮引数
function f(x: number, _y: boolean) { console.log(x) }
noUnusedParamete
を true
にすると使っていない引数があると怒られるのだが、 _
を頭に付けると怒られなくなる。
ということで _
は TS 界では区別な意味を持つので使わないほうが無難。
this
への type annotation
function f(this:string) { this.toUpperCase(); // this の型がわからないと toUpperCase() があるかわからない }
みたいな記法で this
の型を指定できる。引数っぽくみえるけどコンパイルすると消える。キモい。noImplicitThis
は this
に触らなければ type annotation なくても怒られない。
noImplicitReturns
返り値の型を書かないと怒られる...ではなく、暗黙的に undefined
返してたら怒られるので返り値はしっかり書くように。
function f(x: number) { if (x < 0) return 42; } // => error TS7030: Not all code paths return a value.
noEmitHelpers
と importHelpers
TSのヘルパー関数はファイルごとに生成されるため重複することになる。それを防ぐのが noEmitHelpers
でヘルパ関数を生成しなくなる。生成しなくなるだけだと困るので npm i -save tslib
して import "tslib";
をどこかに書けば重複させずにヘルパ関数を生成できる。
これだけだとうっかり import "tslib";
を消してしまった場合が怖い。 なので importHelpers
で import "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
n
は null
または number
なので本来ならば toString()
は使えない(TS の union type は全ての型に共通に存在するプロパティにのみアクセスできる)。が、!
をつけると number
にキャストされ toString()
に呼び出すことができるようになる。
実際 n
は null
なので n!.toString()
したらエラーになるのでやめよう。治安を維持したいなら
if (n != null) { n.toString(); }
みたいに書くべき。!
は所詮キャストなので実際の JS の型とずれることになる。
declara
型定義書くときに必要。片手間で書くのであれば知らなくて良い。
namespace
昔の JS のグローバル汚染防ぐためにオブジェクトに色々生やしてたあれができるキーワード。 対になる概念は module
で import
や export
が書いてあったらそれは module
である。今時 namespace
使うことはほぼ無いと思う。
abstract
とか readonly
とか private
とか
JS にはないわけだけどまぁオブジェクト指向言語にはある一般的な概念なのでそんなに困らないと思う。もちろん TS の仕様は把握する必要あり。
仕様追加の速度が早いのでいつの間にかできるようになっていたりする。 プロパティへの abstract
とか readonly
とか一年前にはなかった気がする。
keyof
in
オーバーロード
あんまり便利じゃないので自分で書く時使うことは思う。
- 型ごとに実装を持てない
- 所詮は JS なので
- 順番とか気にしないといけない
型関連の仕様
すごい勢いで追加されていくので説明している余裕はなし。全部知りたいなら公式ドキュメント見ましょう。
使用頻度が高いやつの紹介だけ。
Union Types
type A = number | string | boolean;
みたいなやつ。 A
型は number
string
boolean
共通のプロパティにしかアクセスできなくなる。
number
か string
か boolean
か判断できないのでこうなっているだけで、実際はどれなのか特定して型を狭めていきたいはず。その仕組みを 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 に移行する
僕自身大規模なプロジェクトを移行させたことないので実際どうなるかは分からないが機能面のサポートとしては
allowJS
.js
をそのまま読み込める
- Untyped imports
noImplicitAny
ならば npm module の型定義不用- https://github.com/Microsoft/TypeScript/pull/11889
があるので、 babel 使ってたというプロジェクトならばなにも考えず babel 部分を tsc に置き換えればいけるはず。
その後は徐々に .ts
に書き換え、型を付けていくとよい。
読むと良いドキュメントなど
- https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html
- あんまり充実はしてないけど一読の価値はある
- https://github.com/Microsoft/TypeScript/wiki/FAQ
- FAQ
- Substitutability 原則とそれが適用されている事例とか読んでおくと納得できることが増えると思う
- https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines
- TS本体のコーディング規約
- 大体は本体のコーディング規約に従うのが得策
- https://github.com/Microsoft/TypeScript/wiki/Roadmap
- ロードマップ
- 日本語で読みたければ http://qiita.com/vvakame
- https://blogs.msdn.microsoft.com/typescript/
- 公式ブログ
- RSSリーダーに入れとけ
- issue から切り貼りしてるので正直なに言ってるかわからない
- https://www.typescriptlang.org/play/
- 昔はオプション選べず不便だったが最近選べるようになったのでシュッと試したいときなどに
- バージョンいくつが動いてるかはわからない