TypeScript 2.1
- keyofとルックアップ(Lookup)型
- Mapped Types
- オブジェクトのSpreadとRest
- ES3とES5でのasync関数のサポート
- 外部ヘルパーライブラリ(tslib)のサポート
- 型宣言無しのインポート
- --target ES2016、--target ES2017、--target ESNextのサポート
- any推論の改善
- リテラル型の推論の向上
- superの呼び出しの戻り値を'this'として使用
- 設定の継承
- 新しいフラグ--alwaysStrict
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を使用すると、PartialPerson
をPerson
の型から変換して作り出すように書くことが可能です。
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
上記で説明(定義)したPartial
とReadonly
は、非常に便利な構造です。
それらを使用して、次のような共通のJSルーチンを記述できます。
function assign<T>(obj: T, props: Partial<T>): void;
function freeze<T>(obj: T): Readonly<T>;
そのため、これらは標準ライブラリにデフォルトで含まれています。
また同様に、Record
とPick
という別の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 };
同様に異なる幾つかのオブジェクトをマージすることも可能です。
下記の例では、merged
はfoo
、bar
、baz
からのプロパティを持ちます。
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
が存在することを把握するようにする必要もあります。
{
"compilerOptions": {
"lib": ["dom", "es2015.promise", "es5"]
}
}
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つの選択肢が存在していました。
- それらを必要とする各ファイルにヘルパーを注入する。
-
--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等の組み込みのクラスを拡張する動作が破壊されます。 詳細については、組み込みの拡張の破壊的変更を参照してください。
設定の継承
プロジェクトはしばしば複数の出力ターゲットを持つことがあります。
例えばES5
とES2015
、デバッグ用と製品版、CommonJS
とSystem
などがあります。
これらの2つのターゲット間で幾つかの設定オプションを変更したり、
複数のtsconfig.json
ファイルを管理するのは非常に面倒です。
TypeScript 2.1はextends
を使用した設定の継承をサポートします。
-
extends
はtsconfig.json
内の新しい最上層のプロパティです。 (compilerOptions
、files
、files
、include
、exclude
に並んで) -
extends
の値は、継承する別の設定ファイルへのパスを含む文字列でなければなりません。 - 基になるファイルの設定がまず読み込まれ、継承したファイルの設定が上書きされます。
- 設定ファイル間の循環は禁止されています。
-
継承したファイルの
files
、include
、exclude
は、継承元のファイルのものを上書きします。 - 設定ファイル内の全ての相対パスは、それらの設定ファイルを出発点とする相対で解決されます。
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true
}
}
{
"extends": "./configs/base",
"files": [
"main.ts",
"supplemental.ts"
]
}
{
"extends": "./tsconfig",
"compilerOptions": {
"strictNullChecks": false
}
}
新しいフラグ--alwaysStrict
--alwaysStrict
を使用してコンパイルすると、次のことが起こります。
- 全てのコードがstrictモードで分析されます。
-
生成された全てのファイルの先頭に、
"use strict";
の指示を書き込みます。
モジュールは自動的にstrictモードで分析されます。 モジュールでは無いコードで、この新しいフラグを使用することが推奨されます。
© https://github.com/Microsoft/TypeScript-Handbook
このページは、ページトップのリンク先のTypeScript-Handbook内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。
- ドキュメントの情報が古い可能性があります。
- "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。