TypeScript 2.1.4 変更点

  • 68
    いいね
  • 0
    コメント

まさかこんなことになるなんて。

こんばんは@vvakameです。

TypeScript 2.1がアナウンスされましたね。
What's new in TypeScriptも更新されているようです。

RC版である2.1.1から1ヶ月ほどで正式版が出てきました。
なかなか重たい変更がこの期に及んで!追加されているため解説していきます。
なお、2.1.1変更点で解説した内容は扱いません。

既に TypeScript 2.1 で導入される keyof を使って EventEmitter を定義してみるTypeScript 2.1のkeyofとかMapped typesがアツい などの記事が書かれているので、本記事で物足りなかった人は色々と巡回してみるとよいでしょう。

変更点まとめ

keyof と 型の切り出し

keyof 演算子が導入されました。

keyof-a.ts
interface Person {
    name: string;
    age: number;
    location: string;
}

let propName: keyof Person;

keyofの動作の様子

こんな感じの動作です。

さらに、型の切り出しが可能になりました。
(Lookup Types を 型の切り出し と訳すのが妥当かどうか微妙なので広く知られた訳が既に存在していたらコメントかなにかで教えてください…)

keyof-b.ts
interface Person {
    name: string;
    age: number;
    location: string;
}

let a: Person["age"];

// 以前からclassだったら頑張れば似たようなことができた
class Animal {
    kind: string;
    name: string;
}

let b: typeof Animal.prototype.kind;

型の切り出しの様子

便利といえば便利ですね。
"age"部分は入力補完も効きますし、typoすればコンパイルエラーにもなります。
リファクタリングかけた時に一緒に変更されたりはしないようなので多用すると修正がめんどくなる可能性はあります。

さらに、プロパティ名部分にunion typesが使えたり

keyof-c.ts
interface Person {
    name: string;
    age: number;
    location: string;
}

// string | number 型
let nameOrAge: Person["name" | "age"];

Genericsと組み合わせた演算処理っぽいのもできるそうです。
ここまでやるかこの変態!(JavaScriptの実用上普通にこういう処理あるので必要といえば必要

keyof-d.ts
function get<T, K extends keyof T>(obj: T, propertyName: K): T[K] {
    return obj[propertyName];
}

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

let foo = get(x, "foo"); // has type 'number'
let bar = get(x, "bar"); // has type 'string'

let oops = get(x, "wargarbl"); // error!

これを突き詰めていくと Object.defineProperty 的なものでも型チェックできそうです。

keyof-e.ts
interface PropertyDescriptor<T> {
    configurable?: boolean;
    enumerable?: boolean;
    value?: T;
    writable?: boolean;
    get?(): T;
    set?(v: T): void;
}
function defineProperty<T, K extends keyof T>(o: T, p: K, attributes: PropertyDescriptor<T[K]>): any {
    return Object.defineProperty(o, p, attributes);
}

interface Foo {
    a?: string;
}

let foo: Foo = {};

// 正しい組み合わせ a に string
defineProperty(foo, "a", {
    enumerable: false,
    value: "a",
});
// ダメ a に number
defineProperty(foo, "a", {
    enumerable: false,
    value: 1,
});
// ダメ b は存在しない
defineProperty(foo, "b", {
    enumerable: false,
    value: "a",
});

すごい。
この機能はハード型定義クリエイター以外の人も普通にコードを書いていて使う必要に迫られる可能性があるのがヤバいです。
万人が使いこなせる気配が全くしないので、ある程度の keyof を使った処理のsnippetとかをみんなで育てたほうがよいのでは…。

ある型のフィールドの修飾子の変換(Map処理)が可能に

2.1.4はヤバい機能盛りだくさんなの??
型のMap処理ができるようになりました。
ちょっと理解が追いついてるのか完全に怪しい…。

基本操作は次の4種類だそうです。

{ [ P in K ] : T }
{ [ P in K ] ? : T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ? : T }

英語話者だとスッと理解できるのかもしれないけど難しいですね。
K の中の P の値にあたる T と読めばいいのでしょうか。

TypeScriptの標準型定義の中にいくつかのパーツが同梱されているのでまずはその定義を見てみましょう。

// 各プロパティをoptional ? にする
type Partial<T> = {
    [P in keyof T]?: T[P];
};

// 各プロパティを読取専用にする (immutable化)
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// 一部のプロパティのみ集めた部分集合
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}

// Genericsと組み合わせて写像を作る用ぽい
type Record<K extends string, T> = {
    [P in K]: T;
}

これをざっくりこう使うようです。

mapped-types-a.ts
interface Person {
    name: string;
    age: number;
    location?: string;
}

let p1: Person = {
    name: "vvakame",
    age: 32,
    location: "Tokyo",
};

let p2: Partial<Person> = {
    name: "vvakame",
    // age, location が欠けていてもエラーにならない
};

let p3: Readonly<Person> = {
    name: "vvakame",
    age: 32,
};
p3.name = "TypeScript"; // readonly なのでエラーになる

let p4: Pick<Person, "name" | "location"> = {
    name: "vvakame",
    // age は K に含まれていないので不要
    location: "Tokyo", // 必須になる
};

let p5: Record<keyof Person, boolean> = {
    // 全てのプロパティの型はbooleanを要求される
    name: true,
    age: true,
    location: false, // 必須になる
};

難易度が高い。
上手く使えば次のような変換処理も動くそうな。

interface Foo {
    a: string;
    b: string;
}

function mapObject<K extends string, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U> {
    let newObj: any = Object.assign({}, obj);
    Object.keys(obj).forEach(key => newObj[key] = f(newObj[key]));
    return newObj;
}

// result は Record<"name", number>
let result = mapObject({name: "vvakame"}, v => v.length);
// { name: 7 } と表示される
console.log(result);

入力補完もちゃんと効くし型も正しく認識されている

Mapped Typesの様子

ここでReactのpropsみたいな複雑な型も上手く扱えるのはか興味があるところです。

Object Rest/Spread Properties for ECMAScript が入った

ArrayとかにあったやつがObjectにも来た的なやつです

let original = {
    1: 1, 2: 2, 3: 3, 4: 4, 5: 5,
    6: 6, 7: 7, 8: 8, 9: 9, 10: 10,
    11: 11, 12: 12, 13: 13, 14: 14, 15: 15,
};

// コピーとかできます
let copy = { ...original };

let fizz = { 3: "foo", 6: "foo", 9: "foo", 12: "foo", 15: "foo" };
let buzz = { 5: "buzz", 10: "buzz", 15: "buzz" };
let fizzbuzz = { 15: "fizzbuzz" };

// mergeとかできるで
let merged = { ...copy, ...fizz, ...buzz, ...fizzbuzz };
// { '1': 1,
//   '2': 2,
//   '3': 'foo',
//   '4': 4,
//   '5': 'buzz',
//   '6': 'foo',
//   '7': 7,
//   '8': 8,
//   '9': 'foo',
//   '10': 'buzz',
//   '11': 11,
//   '12': 'foo',
//   '13': 13,
//   '14': 14,
//   '15': 'fizzbuzz' }
console.log(merged);

let animals = {
    cat: "😺",
    dog: "🐶",
    rat: "🐭",
};
let { cat, ...others } = animals;
// 😺
console.log(cat);
// { dog: '🐶', rat: '🐭' }
console.log(others);

superを呼び出しした時コンストラクタでreturnした値がthisとなるように変更

そういう仕様がECMAScriptにあったけど完全に知らなかった…
http://www.ecma-international.org/ecma-262/6.0/index.html#sec-super-keyword

コンストラクタ内部で任意の値をreturnするとその値が生成された事になります。
Chromeとかで var obj = new class { constructor() { return new Date(); } }(); とかやるとobjは普通にDateになります。

この仕様は特にCustom Elements周りで必要らしいです。

super-returns-this.ts
class Base {
}

class Inherit extends Base {
    x: string;
    constructor() {
        super();
        this.x = "Hi!";
    }
}

こういうコード書くと

super-returns-this.js
var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var Base = (function () {
    function Base() {
    }
    return Base;
}());
var Inherit = (function (_super) {
    __extends(Inherit, _super);
    function Inherit() {
        var _this = _super.call(this) || this;
        _this.x = "Hi!";
        return _this;
    }
    return Inherit;
}(Base));

こういうコードが出てくる。

returnした値の型がインスタンスの値になるわけではないようなので、returnする値がクラスの制約を満たすように書いてやらねばならない点に注意が必要です。
instanceofの挙動も死にそうだし変に多用してはダメっぽそう。

React.createElement 以外のJSXファクトリが利用可能に

TSX(JSX)書いた時に要素の組み立てに使うファクトリに React.createElement 以外を使うことができるようになりました。
例えば、SkateJSもJSXを採用しているので、このオプションを使う場面があります。
興味がある人は僕が作ったskatejs-todoを参照してみてください。
また、TechBoosterがC91で出すWeb本でもこのあたりの話に触れているので興味がある人はどうぞ!(宣伝

--target ESNext がさらに追加された

このへん ぽい

Object Rest/Spread Properties はまだstage 3でES2017にも入ってないので、これらがdownpileされないためのtargetを追加したようだ。

型付けなしの気軽なimport句

今まで :型定義ファイルが存在しないライブラリは利用できなかった
これから:とりあえず常にimportできて動きます

今まで、型定義ファイルのないライブラリはTypeScriptは信用せず、使い始めるのがめんどくさかったです。
そのため、"書き捨てのコードを書きたい"とか"とりあえず使い始めたい"という時にTypeScriptを使うのは億劫でした。
今回の更新で、"とりあえずnpm installされてるならanyとしてimportできるようにしよう"という方針に転換したようです。
これはなかなかいい話ですね。

なお、今まで通り厳密な運用をしたい!という人は --noImplicitAny を(既に使っているように)使えば従来通りの挙動になります。
ごあんしんです。

余談

前回記事でtslib更新されてなくてアカンわと書いたのですが、ユーザが触れる機会も増えたので高い頻度で更新されるようになりました。
最新のtslibでは __generator など必要なものが一式含まれているため、--importHelpers を使ってよい時期が来たと言えるでしょう。