83
83

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TypeScript 2 のモダンな書き方

Posted at

はじめに

2016年11月に TypeScript 2.1 がリリースされ 、ES2016(ES7) と足並みを揃えつつより現代的なプログラミング言語の機能の大半が揃いました。Angular2 からの採択や TypeScript のネイティブ対応、 Visual Studio および Visual Studio Code の強力なコーディング支援によって、その存在感を増しつつあります。

この記事では TypeScript 2.1 で新たに導入された機能や TypeScript 1.8 頃から入っていった大きな変化をさらいつつ、実際のコードをどう書き換えればよいかを示していきます。

基本的には TypeScript 1.5 から 1.7 くらいで止まっている人が対象ですが、Angular2 を機に入ってきたようなニュービーの方にも巷の古いサンプルコードを読み替える役に立つと思います。

#——というつもりで書いていたのですが、結局11月中の正式リリースはありませんでした。しかしながら大きな変更はないと思うので先行して記事だけ公開します。

No <reference>宣言 [2.0]

もしあなたのコードに /// <reference path="typings.d.ts"/> のような一文がある場合、その書き方は旧来のものです。

知ってのとおり、TypeScript からプレーンな JavaScript のライブラリを利用する場合はなんからの仕組みを用いて型定義ファイルを参照する必要があります(2.1では抜け穴あり。別のセクションで記述)。

以前は typings というツールを使って定義ファイルをインストールし、コード先頭のトリプルスラッシュからはじまるコメントで参照していました(少し前までは typings すらありませんでした)。典型的には以下のように打っていました。

console
$ typings install jquery
index.js
/// <reference path="typings.d.ts"/>

$.ready(() => {
   ...
});

TypeScript 2 ではこの仕組みが一新され、下のように npm だけでインストールとバージョン管理ができるようになっています。コードに特別な記述はいりません。コンパイラが自動で node_modules/@types/ 以下のファイルを参照してくれます。ちなみにこの参照先は tsconfig で変更できます。

console
$ npm install @types/jquery

ただし、 @types に存在するものしか使えません。従来の DefnitelyTyped で提供されていたものはどれも使えるはずですが、外部のリポジトリを参照している場合は注意がいります。

余談

最新の Visual Studio Code ではこの型定義を利用して JavaScript にも TypeScript と同等のコード補完を提供しています。

個人的には ES2020 くらいの未来には型アノテーションの仕組みだけが残ることもありうると思っています。

null 安全[2.0]

「Null 安全でない言語はもはやレガシー言語だ」という記事がありましたが、TypeScript 2 はレガシー言語ではありません。undefinednull はもはや明示的な型定義なしに通常の変数に代入できなくなっています(明示的な設定が必要です。後述)。

公式の例をそのまま引用します。

// Compiled with --strictNullChecks
let x: number;
let y: number | undefined;
let z: number | null | undefined;
x = 1;  // Ok
y = 1;  // Ok
z = 1;  // Ok
x = undefined;  // Error
y = undefined;  // Ok
z = undefined;  // Ok
x = null;  // Error
y = null;  // Error
z = null;  // Ok
x = y;  // Error
x = z;  // Error
y = x;  // Ok
y = z;  // Error
z = x;  // Ok
z = y;  // Ok

falsy でないことをチェックした後は通常の型に代入できるので不便はありません。積極的に使っていきましょう。

古いコードに関してはひとまず有効にしてチェック漏れをなくしていくことをお勧めします(外部のライブラリが足かせになることもあります)。

ちなみにオプション引数や後述するオプションプロパティも同じ扱いになります。

なお、配列の境界外アクセスなどで紛れ込むことまでは防げません。誤解のないように。

補足

互換性のない変更のため、デフォルトでは無効になっています。有効にするにはコンパイラオプションで --strictNullCheck を付けるか、tsconfig で strictNullCheck フィールドを true に設定します。

class定義 [2.0]

抽象クラス

抽象クラスがサポートされました。クラス、メソッド、インデクサなどの前に abstract と書けば使えます。以上。

非publicコンストラクタ

つねに public だったコンストラクタを protected/private にできます。シングルトンの実装で使います。

読み取り専用プロパティ

これも名前の通り。プロパティの前に readonly と書けば使えます。インデクサも同じ。また getter のみのアクセサが自動的に readonly 扱いになります。

オプションプロパティ

interface などで定義してもしなくてもいいプロパティを記述できます。上と違って TypeScript ならではの緩い機能といえるでしょう。

もっとも出番が多いのは関数に渡すオプションでしょう。以下のように使えます。

function getData(url: string, options: {
  onsuccess?: (response: string) => void;
  onerror?: (code: string) => void;
});

React のコンポーネント定義などもこれなしには書けません。

アロー関数によるインスタンスメソッド記述 [?.?]

メソッドの本体をアロー関数で書く記法です。

この場合はインスタンスごとに新しい関数が作成され、直接プロパティにひもづきます。

トランスパイルの結果を見た方が早いでしょう。

arrow.ts
class Example {
    hello() {
        return 'hello';
    }
}

class ExampleArrow {
    hello = () => 'hello';
}
arrow.js
var Example = (function () {
    function Example() {
    }
    Example.prototype.hello = function () {
        return 'hello';
    };
    return Example;
}());

var ExampleArrow = (function () {
    function ExampleArrow() {
        this.hello = function () { return 'hello'; };
    }
    return ExampleArrow;
}());

これを用いるとイベントハンドラを定義する際に必要になる bind の呼び出しを取り去ることができます(Bind is Harmful を参照)。

ただ ES の仕様案としては今のところドラフト(stage 2)の段階なので、実戦で使うには気が早いかもしれません。

ES2016/2017対応 [2.1]:new:

TypeScript 2.1 から出力先の言語に ES2016(ES7), ES2017(ES8) を選べるようになりました。target オプションで指定できます。

同時に Object に対するスプレッド演算子...と残余演算(ES2016)、先述の async/await(ES2017) のダウンパイルが新たにサポートされました。

それ以前の出力先でも ES2015 のほとんどの機能と ES2016 以降の機能の一部が使えます。制限はありますが、徐々に対応を広げています。機能によっては tsconfig での設定が必要です。

tsconfig.json
{
    "compilerOptions": {
        "lib": ["es2016"] //"es7" でも可。
                          //ちなみに tsconfig においてはコメントは合法(1.6以降)。
     }
}

 
この lib も 2.0 で追加されたオプションで、組み込みで使えるライブラリをコンパイラに教えます。あくまで型定義であって Polyfill ではないので、必要に応じて core.js などを併用してください。

async/await [2.1]:new:

TypeScript 1.6 で導入された非同期処理を同期処理のように書く機能ですが、2.1 ではついに ES5 以前へのダウンパイルがサポートされました。

ES2017 で本家にも取り込まれる予定(ステータスは候補?(https://tc39.github.io/ecmascript-asyncawait/#status) )で、Chrome, Firefox, Opera は実装済みです。

Node.js はもちろんブラウザにおける新しめの機能も非同期が基本です。コールバック地獄よりマシという程度の Promise チェインにそろそろ別れを告げましょう。

//非同期でファイルを読む関数
function readFileAsText(file: File): PromiseLike<string> {
    return new Promise<string>((resolve, reject) => {
        const reader = new FileReader();
        reader.onloadend = () => resolve(<string>reader.result);
        //エラー時の reject は略。
        reader.readAsText(file);
    });
}

Promise ベースの書き方:

function handleFiles(files: File[]) {
    return Promise.all(files.map(file => readFileAsText(file))); 
    //これはいったい...?
}

async/awaitベースの書き方:

async function handleFiles(files: File[]) {
    for (let file of files) {
        await readFileAsText(file);
    }
    //やはり、こうでしょう。
}

ダウンパイルは Promise を前提とした実装なので、tsconfig で lib の設定が必要です(もちろん実行環境にも。ない場合は Polyfill なり上位互換のライブラリなりを入れましょう)。

tsconfig.json
{
    "compilerOptions": {
        "target": "es5",
        "lib": ["dom", "es2015"] // or ["dom", "es2015.promise", "es5"]
    }
}

ちなみに ES2015 では TypeScript 1.7以降で generator と Promise を用いた実装になっています。

型定義なし import [2.1]:new:

2.1では型定義ファイルがないモジュールの import が許されるようになります。

これは地味に大きな変更で「TypeScript 使いたいんだけどXXXライブラリが対応していなくて……」という声がなくなります。最新バージョンを利用するためにあえて型なしで使うという選択肢もあるでしょう。

この場合、読み込んだモジュールの型はつねに any になります。そのため noImplicitAny オプションと同時には使えません。

少し前のバージョンで追加された allowJS オプションのモジュール版と考えてもよいでしょう。

豆知識

よく誤解されるのですが、TypeScript の import は必ずしも import/require などのコードを生成しません。

読み込んだメンバーが interfacetype のような型に属するものであれば型チェックのみが行われます。コードが生成されるのは class, function, namespace, var, let, const で定義された変数を読み込んだ場合のみです。その場合も変数がすべて未使用であれば何も生成されません。

import の仕様変更[1.8]

本来クライアントサイドのみで使う人には関係のない話ですが、Browserify や webpack を用いる場合に効いてくるので、やはり知っておくとよいでしょう。

TypeSciprt 1.8 から import文におけるモジュール解決の仕様が Node.js の流儀に変更されています。

詳しいメカニズムは typescriptlang の文書 にまとまっています。最低でも、import 'foo' は node_modules フォルダの中を探しにいくもので import './foo' とは動作と意味合いが異なる、という点は頭に入れておきましょう。

用例集

実用上は以下の形で覚えておけば事足りるでしょう。拡張子.ts(x)をつけないように注意してください。

まずは npm でインストールしたモジュールについて。

グローバルに展開すべきモジュールは以下のようにします。

import 'jsdom'; //グローバル(window)に展開される。Bare import と呼ばれる。

通常のモジュールは以下のようにします。

import * as Q from 'q' //モジュール全体を指定の名前のオブジェクトで受ける。 Q.Promise のようにメンバーを参照する。
import {Promise, when} from 'q'; //モジュールの一部を元の名前で受ける。
import AppBar from 'material-ui/AppBar'; //モジュール内の1ファイルの default export を受ける。

npm 以外で追加したモジュールについて。

import Q from './bower_components/q'; //相対パスでモジュールファイルの default export を受ける。

例は書きませんが as{} も使えます。

自作のファイルは以下のように受けます。

import MyFancyButton from './components/MyFancyButton'; //相対パスで自作の.tsファイルの default export されたメンバーを受ける。
import {MyQueryBuilder, MyQueryResult} from './my-great-query'; //相対パスで自作の.tsファイルの export されたメンバーの一部を受ける。

補足

繰り返しますが、あくまでモジュールの仕組みに CommmonJS を指定した場合の挙動です。ES6/ES2015 や AMD, SystemJS などを指定した場合はこうなりません。

CommonJS 下で従来通りのモジュール解決を行いたい場合はコンパイルオプションで moduleResolutionclassic を設定します。

ほかにもモジュール解決に絡んだ緊急避難的なオプションがいくつか追加されていますので、困ったときは調べてみましょう。

TypeScript の import はその柔軟性ゆえに最も挙動のわかりづらい機能のひとつです。よそのコードを読むときは前提とする環境と module オプションの設定を確認したほうがよいでしょう。

TIPS

ライブラリの型定義ファイルを自作するときはこれを利用して、自分用の node_modules フォルダを用意すると便利です。.gitignore で無視されていないかだけ気をつけましょう。


+ root/
  +- node_modules/
  |  +- @types/
  |     |- react/
  |     |- react-dom/
  |  +- react/
  |  +- react-dom/
  |  +- react-thunk/
  |  +- semantic-ui-react/
  +- src/
     +- node_modules/
     | +- semantic-ui-react.d.ts
     | +- react-thunk.d.ts
     +- index.ts

ここで

  • root はプロジェクトのルート(package.jsonがあるところ)
  • src はソースフォルダの最上位
  • semantic-ui-react.d.tsreact-thunk.d.ts が自作の型定義ファイル

になります。

JSX Factory [1.8]

TypeScript では1.6でJSXのコンパイルにネイティブで対応して以来、サポートを広げており、1.8 ではさらに jsx factory と呼ばれる機能が追加されます。これは React 以外で JSX 記法を使うための機能で、babel にあるので入れようという流れのようです。

使うにはある名前空間に createElement関数(<element/>および<Component/>相当)と__spread関数(<xxx ...props/> 相当)を定義したうえで、コンパイラの reactNamespace オプションにその名前空間を指定します。

これ自体の用途は限定的ですが、最近の TypeScript は React まわりの機能拡充に余念がなく、型定義ファイルに公式にコミットしたりtslint を提供したりするほど力を入れているのがわかると思います。

どのみちトランスパイラがいるうえに Props の定義がややこしい React はかなり TypeScript 向きです。ただし、いまの TypeScript はダウンパイルのサポート範囲で若干 babel に劣ります(両方組み合わせることもできますが)。

型システムの厳密化

ほかにもライブラリをつくる際や型定義ファイルを書く上で便利な機能がいろいろ追加されています。

一ユーザーとして使う機会は少ないかもしれませんが、まとめて紹介します。

インデクサの型指定 [1.8]

文字通りの機能で自作のコレクション型などに使います。

type Book = {
    title: string;
    author: string;
};

type BookList = {
  /**/[i: number]: MyItem;
};

ES6で標準のコレクション型ができたこともあり、出番は多そうです。

タグつき共用体(判別共用体) [2.1]:new:

名前が専門的ですが、|演算子による型定義をさらに拡大して、「円か四角か長方形であるところの図形型」のようなものを定義する機能です。

利用するにはすべての型にその種別を表す同名のプロパティを持たせて判別を行います。型推論が賢いので判別後は実際の型で認識されます。

これも公式の例をあげます。

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

interface Circle {
    kind: "circle";
    radius: number;
}

type Shape = Square | Rectangle | Circle;

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.width * s.height;
        case "circle": return Math.PI * s.radius * s.radius;
    }
}

イベント型などで用いるとより厳密な型安全性を手に入れることができるでしょう。

補足:共用体と|演算子について

そもそも共用体自体を見慣れない人も多いと思いますので、簡単に補足しておきます。

一言でいうと AB どちらかに当てはまる型を要求するとき、A | B と書ける機能です。

たとえば関数のオプションで、文字列か文字列の配列を受け取るものなどは、 any でなく string | string[] のようにすることでより厳密にエラーを弾けます。

上にもあるように、TypeScript(1.8以上) では文字列リテラルが型の一種なので、UIライブラリでは以下のような記述がよくされます。

type Style = {
    textAlign: 'left' | 'center' | 'right' | 'justify';
}

Union Type の補完的な機能として Intersection Type& 演算子というものがあります。これはすべての型に当てはまる型を要求するときに用います。

never 型 [2.0]

TypeScript 2ではフロー解析の強化とともに never という特殊な型が加わりました。

例外を投げるフローや無限ループを表すのに使います。プログラマが明示的に書くケースは少ないかも知れません。

関数内 this の型 [2.0]

イベントオブザーバーやコールバックなどで this をつけて関数を呼び出す(call または apply)場合にその型を指定できます。

下のように書きます。

interface ButtonProps {
    onClick(this: Button, event: Event);
}

ライブラリでは使いどころの多い機能なので、広めていきたいところです。

UMDモジュール [2.0]

TypeScript 2では UMD モジュールのための記法がサポートされます。

クライアント、サーバー環境問わずに利用される Q を例にとると、現時点のDefinitelyTypedの最新 ではつぎのようになっています。これは従来の CommonJS 的なモジュール(より旧い呼称では外部モジュール)の記法です。

Q.d.ts
declare function Q<T>(promise: Q.IPromise<T>): Q.Promise<T>;
declare function Q<T>(value: T): Q.Promise<T>;
declare function Q(): Q.Promise<void>;
declare namespace Q {
    //export いろいろ
}

declare module "q" {
    export = Q;
}

一方、@types に移された 2.0向けのファイル ではつぎのようになっています。この export as namespace Q がUMDモジュール向けの新しい記法です。

q/index.d.ts
export = Q;
export as namespace Q;

declare function Q<T>(promise: Q.IPromise<T>): Q.Promise<T>;
declare function Q<T>(value: T): Q.Promise<T>;
declare function Q(): Q.Promise<void>;
declare namespace Q {
    //export いろいろ
}

その他

だいぶ長くなりましたが、これでもすべては網羅していません。あとはつぎのセクションのリンクから公式 Wiki の What's New in TypeScript を参照してください。

典拠

おまけ

執筆時は TypeScript 2.1 は正式ではありません。

そのため、Play ground では 2.0 までの機能しか試せません。コンパイルオプションも(私の知るかぎり)設定できません。

それらの機能を試したい人はローカルに環境にインストールする必要があります。

ここではプロジェクト専用のフォルダに TypeScript 2.1 をインストールして Visual Studio Code で編集できるようになるまでの手順を紹介します。

インストールはコンソールから以下のコマンドで行います(Node.js 環境はすでにあるものとします)。 明示的なバージョン指定が必要なことに注意してください。

mkdir ts2-test
cd ts2-test
npm init
# (Enter/Return を連打)
npm install --save typescript@2.1

Visual Studio Code にバンドルされているのも2.0のままなので、追加の設定が必要になります。

上で作成したフォルダの .vscode/ にある内容の設定ファイルを作成します。コンソールから次のコマンドを実行してください。

mkdir .vscode
echo "{\"typescript.tsdk\": \"./node_modules/typescript/lib\"}" > .vscode/settings.json

これで Visual Studio Code 上でフォルダ内の TypeScript パッケージが使われるようになります。

詳しくは https://code.visualstudio.com/docs/languages/typescript#_using-newer-typescript-versions を参照してください。

83
83
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
83
83

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?