TypeScript 2.1

keyofとルックアップ(Lookup)型

JavaScriptで、プロパティ名がパラメータとして必要とされるAPIを持つことはかなり一般的ですが、 これまでのところ、これらのAPIで発生する型の関係を表現することはできませんでした。

Enter Index Type Query or keyof; 索引付けされた型問合せ(indexed type query)keyof Tは、Tに対して許可されるプロパティ名の型をもたらします。 keyof Tの型は文字列の部分型であるとみなされます。

interface Person {
    name: string;
    age: number;
    location: string;
}

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[];  // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person };  // string

この2つは索引付けされたアクセス型(indexed access types)で、ルックアップ型(lookup types)とも呼ばれます。 構文的には、要素へのアクセスと全く同じように見えますが型として書き出されます。

type P1 = Person["name"];  // string
type P2 = Person["name" | "age"];  // string | number
type P3 = string["charAt"];  // (pos: number) => string
type P4 = string[]["push"];  // (...items: string[]) => number
type P5 = string[][0];  // string

このパターンを型システムの他の部分と一緒に使用すると、型セーフなルックアップを取得することができます。

function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];  // 型はT[K]と推論
}

function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) {
    obj[key] = value;
}

let x = { foo: 10, bar: "hello!" };

let foo = getProperty(x, "foo"); // number
let bar = getProperty(x, "bar"); // string

let oops = getProperty(x, "wargarbl"); // Error! "wargarbl" is not "foo" | "bar"

setProperty(x, "foo", "string"); // Error!, string expected number

Mapped Types

一般的なタスクの1つに、既存の型を取得して、それぞれのプロパティをひたすら任意にするというものがあります。 下記のPersonを例にみてみましょう。

interface Person {
    name: string;
    age: number;
    location: string;
}

これの部分的(pertial)なバージョンは次のようになるでしょう。

interface PartialPerson {
    name?: string;
    age?: number;
    location?: string;
}

Mapped typesを使用すると、PartialPersonPersonの型から変換して作り出すように書くことが可能です。

type Partial<T> = {
    [P in keyof T]?: T[P];
};

type PartialPerson = Partial<Person>;

Mapped typesは、リテラル型の集まりから作り出され、新しいオブジェクト型のためにプロパティの集まりが算出されます。

これらはPythonのリストの内包表記(list comprehensions in Python)のようですが、 リストに新しい要素を作り出すのではなく、型の中に新しいプロパティを作り出します。

部分的(Partial)な取り出しに加えて、Mapped Typesは、型に対して多くの有用な変換を表現できます。

// 同じ型を保持するが、各プロパティをread-onlyにする
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// 同じプロパティ名だが、値を固定のものにする代わりにpromiseにする
type Deferred<T> = {
    [P in keyof T]: Promise<T[P]>;
};

// Tのプロパティをプロキシ(getter、setter)でラップする
type Proxify<T> = {
    [P in keyof T]: { get(): T[P]; set(v: T[P]): void }
};

Partial、Readonly、Record、Pick

上記で説明(定義)したPartialReadonlyは、非常に便利な構造です。 それらを使用して、次のような共通のJSルーチンを記述できます。

function assign<T>(obj: T, props: Partial<T>): void;
function freeze<T>(obj: T): Readonly<T>;

そのため、これらは標準ライブラリにデフォルトで含まれています。

また同様に、RecordPickという別の2つの有用な型も含まれています。

// Tからプロパティの集まりであるKを取り出す
declare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;

const nameAndAgeOnly = pick(person, "name", "age");  // { name: string, age: number }
// 型Tの各プロパティKを、Uに変換する
function mapObject<K extends string | number, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>

const names = { foo: "hello", bar: "world", baz: "bye" };
const lengths = mapObject(names, s => s.length);  // { foo: number, bar: number, baz: number }

オブジェクトのSpreadとRest

TypeScript 2.1で、ES2017のSpreadとRestがサポートされました。

配列のSpreadと同様に、オブジェクトのSpreadはシャロー(浅い)コピーを取得するのに便利です。

let copy = { ...original };

同様に異なる幾つかのオブジェクトをマージすることも可能です。 下記の例では、mergedfoobarbazからのプロパティを持ちます。

let merged = { ...foo, ...bar, ...baz };

また、既存のプロパティの上書きと、新しいプロパティの追加も可能です。

let obj = { x: 1, y: "string" };
var newObj = {...obj, z: 3, y: 4}; // { x: number, y: number, z: number }

スプレッドの指定する順序によって、結果として得られるオブジェクトが、どのようなプロパティになるかが決定されます。 後ろにあるスプレッドのプロパティは、その前に作成されたプロパティに「勝つ」ことになります。

オブジェクトのRestは、オブジェクトのSpreadとの二面性の関係があり(翻訳に自信なし)、 要素の分割代入で明示的に選択すること無く、プロパティの集まりを抜き出すことができます。

let obj = { x: 1, y: 1, z: 1 };
let { z, ...obj1 } = obj;
obj1; // {x: number, y:number};

ES3とES5でのasync関数のサポート

この機能はTypeScript 2.1以前にサポートされていましたが、対象がES6/ES2015に限られていました。 TypeScript 2.1では、これがES3とES5のランタイム時に機能するようになり、 環境を気にすることなく、この利点を享受することができるようになりました。

注意:
はじめに、実行時にECMAScriptに準拠したPromiseがグローバルに利用できることを確認する必要があります。 これには、Promise用のポリフィルを取得すること、実行時に対象としているものに依存することが含まれます。

また、libフラグに"dom", "es2015""dom", "es2015.promise", "es5"のように設定することで、 TypeScriptがPromiseが存在することを把握するようにする必要もあります。

tsconfig.json
{
    "compilerOptions": {
        "lib": ["dom", "es2015.promise", "es5"]
    }
}
dramaticWelcome.ts
function delay(milliseconds: number) {
    return new Promise<void>(resolve => {
        setTimeout(resolve, milliseconds);
    });
}

async function dramaticWelcome() {
    console.log("Hello");

    for (let i = 0; i < 3; i++) {
        await delay(500);
        console.log(".");
    }

    console.log("World!");
}

dramaticWelcome();

コンパイルしてその出力を実行すると、ES3/ES5のエンジンで正しく動作するはずです。

外部ヘルパーライブラリ(tslib)のサポート

TypeScriptは幾つかのヘルパー関数を注入します。 それには、継承のための__extends、オブジェクトリテラルとJSX要素のSpread演算子のための__assign、 async関数のための__awaiterなどがあります。

これまでは、2つの選択肢が存在していました。

  1. それらを必要とする各ファイルにヘルパーを注入する。
  2. --noEmitHelpersを使用して、ヘルパーを注入しない。

この2つの選択肢は、より望ましいものになりました。 対象となる全てのファイルにヘルパーを含めることは、パッケージのサイズを小さく抑えたいと考える使用者にとっては悩みの種になっていました。 また、ヘルパーを含まない場合は、使用者が自身でヘルパーのライブラリを保守しなければいけませんでした。

TypeScript 2.1では、これらのファイルを別々のモジュールにしてプロジェクトに組み込むことができるようになり、 コンパイラは必要に応じて、それらをインポートします。

まず、tslibの汎用ライブラリをインストールします。

npm install tslib

次に、--importHelpersを使用して、あなたのファイルをコンパイルします。

tsc --module commonjs --importHelpers a.ts

次(上)の元となるファイルがコンパイルされると、その結果の.jsファイル(下)にはtslibのインポートが含まれており、 インライン展開されることなく__assignヘルパーが使用されています。

export const o = { a: 1, name: "o" };
export const copy = { ...o };
"use strict";
var tslib_1 = require("tslib");
exports.o = { a: 1, name: "o" };
exports.copy = tslib_1.__assign({}, exports.o);

型宣言無しのインポート

TypeScriptは伝統的に、モジュールをどのようにインポートできるのかについて非常に厳格でした。 これは、誤植を避け、ユーザーがモジュールを間違って使用するのを防ぐためでした。

ただし多くの場合が、自身の.d.tsファイルを持たない可能性のある既存のモジュールを、インポートしたいだけだったのかもしれません。 これまでは、これがエラーになっていました。 TypeScript 2.1からは、これがもっと易しくなりました。

TypeScript 2.1では、型宣言を必要とせずにJavaScriptモジュールをインポートすることができます。 型宣言(declare module "foo" { ... }node_modules/@types/fooのような)があれば、依然としてそちらを優先します。

宣言ファイルのないモジュールのインポートには、--noImplicitAnyが有効であれば、依然としてエラーとしてのフラグが立てられます。

// Succeeds if `node_modules/asdf/index.js` exists
import { x } from "asdf";

--target ES2016、--target ES2017、--target ESNextのサポート

TypeScript 2.1は、--target ES2016--target ES2017--target ESNextの3つの新しい対象(target)値をサポートします。

--target ES2016は、コンパイラにES2016固有の機能(例えば、**演算子)を変換しないように指示します。

同様に--target ES2017は、コンパイラにES2017固有の機能(例えば、async/await等)を変換しないように指示します。

--target ESNextは最新のES proposed featuresを対象とします。

any推論の改善

これまでは変数の型が分からなければ、TypeScriptはany型を選んでいました。

let x;      // implicitly 'any'
let y = []; // implicitly 'any[]'

let z: any; // explicitly 'any'.

TypeScript 2.1では、ただanyを選択するのではなく、後で割り当てられるものに基づいて型を推論します。

これは、--noImplicitAnyが設定された場合のみ有効になります。

let x;

// ここでは、'x'に好きなものを割り当てることができます
x = () => 42;

// 直近の割り当ての後、TypeScript 2.1は'x'は'() => number'の型を持つことを把握しています
let y = x();

// ありがたいことに、関数に数値を追加できないことを警告してくれます!
console.log(x + y);
//          ~~~~~
// エラー! 演算子'+'に、型「() => number」と型「number」を適用することは出来ません。

// TypeScriptは、ここでも'x'に割り当てたいものを何でも許可します
x = "Hello world!";

// そして、今は'x'が'string'であるということも把握してます!
x.toLowerCase();

空の配列に対しても、同様のトラッキングが行われます。

型アノテーションが無く、初期値が[]で宣言された変数は、暗黙的にany[]の変数とみなされます。 ただし、その後にx.push(value)x.push(value)x[n] = valueの演算子によって、 何の要素が加えられたかによって変数の型が発展します。

function f1() {
    let x = [];
    x.push(5);
    x[1] = "hello";
    x.unshift(true);
    return x;  // (string | number | boolean)[]
}

function f2() {
    let x = null;
    if (cond()) {
        x = [];
        while (cond()) {
            x.push("hello");
        }
    }
    return x;  // string[] | null
}

暗黙のanyのエラー

これの大きな利点の1つは、--noImplicitAnyを指定して実行したときの暗黙的なanyのエラーの発生を、 少なく抑えられることです。 暗黙的なanyのエラーは、コンパイラが型アノテーション無しでは変数の型を知ることができない場合にのみ報告されます。

function f3() {
    let x = [];  // エラー: 変数'x'は、その型を特定できない場所で暗黙的に'any[]'型を持ちます。
    x.push(5);
    function g() {
        x;    // エラー: 変数'x'は、暗黙的に'any[]'型を持ちます。
    }
}

リテラル型の推論の向上

文字列型、数値型、ブーリアン型のリテラル型(「"abc"」、「1」、「true」など)は、 明示的な型のアノテーションがある場合にのみ前もって推論されていました。

TypeScript 2.1からは、const変数とreadonlyプロパティは、リテラル型が常に推論されます。

型アノテーションが無いconst変数またはreadonlyプロパティーに推論される型は、 リテラル初期化子(literal initializer)の型になります。

let変数、var変数、パラメーター、まreadonlyではないプロパティへ、 初期化子あり且つ型アノテーション無しで推論される型は、初期化子のリテラル型を拡大(widened)したものになります。

拡大される(widened)型は、それぞれ文字列リテラル型ならばstringに、 数値リテラル型ならばnumberに、 trueまたはfalseであればbooleanに、 enumリテラル型であれば、enumを含めるものになります。

const c1 = 1;       // Type 1
const c2 = c1;      // Type 1
const c3 = "abc";   // Type "abc"
const c4 = true;    // Type true
const c5 = cond ? 1 : "abc";  // Type 1 | "abc"

let v1 = 1;   // Type number
let v2 = c2;  // Type number
let v3 = c3;  // Type string
let v4 = c4;  // Type boolean
let v5 = c5;  // Type number | string

リテラル型の拡大(widening)は、明示的な型アノテーションによる制御が可能です。 具体的には、リテラル型の式が型アノテーションの無いconstへ推論された際に、 const変数は、リテラル型と推論されて拡大されます。

ただし、constの場所に明示的なリテラル型の注釈がある場合は、const変数のリテラル型は拡大されません。

const c1 = "hello";  // Widening type "hello"
let v1 = c1;         // Type string

const c2: "hello" = "hello";  // Type "hello"
let v2 = c2;                  // Type "hello"

superの呼び出しの戻り値を'this'として使用

ES2015ではオブジェクトを返すコンストラクタは、super()の任意の呼び出し元を、暗黙的にthisの値に置き換えます。 その結果、(オブジェクトを返すのであれば)super()の戻り値を取得して、thisに置き換える必要があります。

この変更により、Custom Elementsの操作が可能になります。 これを利用して、ユーザーが作成したコンストラクタを使用してブラウザに割り当てられた要素を初期化します。

class Base {
    x: number;
    constructor() {
        // `this`ではなく、新しいオブジェクトを返す
        return {
            x: 1,
        };
    }
}

class Derived extends Base {
    constructor() {
        super();
        this.x = 2;
    }
}

これは、下記を生成します。

var Derived = (function (_super) {
    __extends(Derived, _super);
    function Derived() {
        var _this = _super.call(this) || this;
        _this.x = 2;
        return _this;
    }
    return Derived;
}(Base));

この変更により、Error、Array、Map等の組み込みのクラスを拡張する動作が破壊されます。 詳細については、組み込みの拡張の破壊的変更を参照してください。

設定の継承

プロジェクトはしばしば複数の出力ターゲットを持つことがあります。 例えばES5ES2015、デバッグ用と製品版、CommonJSSystemなどがあります。

これらの2つのターゲット間で幾つかの設定オプションを変更したり、 複数のtsconfig.jsonファイルを管理するのは非常に面倒です。

TypeScript 2.1はextendsを使用した設定の継承をサポートします。

  • extendstsconfig.json内の新しい最上層のプロパティです。 (compilerOptionsfilesfilesincludeexcludeに並んで)
  • extendsの値は、継承する別の設定ファイルへのパスを含む文字列でなければなりません。
  • 基になるファイルの設定がまず読み込まれ、継承したファイルの設定が上書きされます。
  • 設定ファイル間の循環は禁止されています。
  • 継承したファイルのfilesincludeexcludeは、継承元のファイルのものを上書きします。
  • 設定ファイル内の全ての相対パスは、それらの設定ファイルを出発点とする相対で解決されます。
configs/base.json
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}
tsconfig.json
{
  "extends": "./configs/base",
  "files": [
    "main.ts",
    "supplemental.ts"
  ]
}
tsconfig.nostrictnull.json
{
  "extends": "./tsconfig",
  "compilerOptions": {
    "strictNullChecks": false
  }
}

新しいフラグ--alwaysStrict

--alwaysStrictを使用してコンパイルすると、次のことが起こります。

  1. 全てのコードがstrictモードで分析されます。
  2. 生成された全てのファイルの先頭に、"use strict";の指示を書き込みます。

モジュールは自動的にstrictモードで分析されます。 モジュールでは無いコードで、この新しいフラグを使用することが推奨されます。

 Back to top

© https://github.com/Microsoft/TypeScript-Handbook

このページは、ページトップのリンク先のTypeScript-Handbook内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。

  • ドキュメントの情報が古い可能性があります。
  • "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。