Edited at

TypeScript 2.1のkeyofとかMapped typesがアツい

More than 1 year has passed since last update.


About me

@Quramy


  • フロントエンドエンジニア的なことをして生きています。

  • TypeScript + AngularでSPA作ってます。



2.1 RC is coming!

2016.11.08現在、2.1 RCがリリースされた。

TypeScript blogによると、


  • async/awaitのDown transpileがES5/ES3でも利用可能に(~2.0ではES2015以上をtargetにしないと使えなかった)

  • 型推論がより賢くなった

と書いてある。

2.1.1にどのような変更が入ったかは、 @vvakame先生の記事 に詳しく記載されてる



2.1.4

2.1のRoadmap を読むと、


  • Static types for dynamically named properties

  • Mapped types

というのが載っている。今日の本題はこいつら。



Static types for dynamically named properties

https://github.com/Microsoft/TypeScript/pull/11929

feature 名長い。。。

そして「動的に命名されたプロパティのための静的型」とか、最早何のこっちゃ感。

このfeatureでは、keyofという新しいキーワードと、T[K] という2つの記法が追加された。



keyof

keyof T で「type Tのプロパティ名の直和型」を表現するtypeが記述できる

interface User {

name: string;
age: number;
}

type UserKey = keyof User;

と書くと、 UserKey'name' | 'age' というtypeになるよ、ということ。



T[K]

T[K] で「T に対してK型でアクセスして得られるtype」を表現できる

K は、keyof T を満たすtypeであれば良いので、例えば

type UserName = User['name'];

とすると、UserName のtypeは string になる。 User['age'] とすれば number



利用例

interface Observable<T> {

pluck<K extends keyof T>(key: K): Observable<T[K]>;
}

let user$: Observable<User>;
let name$ = user$.pluck('name'); // Observable<string>

以前は name$ = user$.pluck<string>('name') のように、動的にアクセスした先の型情報を自分で補う必要があった。



Mapped types

https://github.com/Microsoft/TypeScript/pull/12114

言ってしまえば(半)動的にtypeを生成する機能

{[P in K]: T}

という記法が可能に。K は stringにアサイン可能なtypeであればよく、 K に対する値は T というtypeになるよ、と言う意味。

type Item = { a: string, b: number, c: boolean };

type T1 = { [P in "x" | "y"]: number }; // { x: number, y: number }
type T2 = { [P in "x" | "y"]: P }; // { x: "x", y: "y" }
type T3 = { [P in "a" | "b"]: Item[P] }; // { a: string, b: number }
type T4 = { [P in keyof Item]: Date }; // { a: Date, b: Date, c: Date }
type T5 = { [P in keyof Item]: Item[P] }; // { a: string, b: number, c: boolean }
type T6 = { readonly [P in keyof Item]: Item[P] }; // { readonly a: string, readonly b: number, readonly c: boolean }
type T7 = { [P in keyof Item]: Array<Item[P]> }; // { a: string[], b: number[], c: boolean[] }


Mapped typesにより、既存typeのkey情報を再利用しつつ、新しいtypeを定義することができる:

type Freeze<T> = {

readonly [P in keyof T]: T[P];
};

let frozenUser: Freeze<User>;
frozenUser.age = 18; // error TS2540: Cannot assign to 'age' because it is a constant or a read-only property.



利用例1

オブジェクトの各key & valueに対して処理を行う関数の定義:

declare function mapObject<K extends string, T, U>(

obj: {[P in K]: T},
fn: (x: T, k?: K) => U
): {[P in K]: U};

const nameLengths= mapObject({
firstName: 'Yosuke',
lastName: 'Kurami'
}, (x => x.length));

// type of nameLengths: { firstName: number, lastName: number};

なお、type Record<K extends string, T> = {[P in K]: T} というtypeがlib.es6.d.tsに定義済み.



利用例2

Boxingのサンプル(Angular2のReactive FormGroupを題材にしました)

interface FormControl<S> {

setValue(value: S): void;
reset(): void;
valueChanges: Observable<S>;
/* etc... */
}

type FormControls<T, K extends keyof T> = {[P in K]: FormControl<T[P]>};

interface FormGroup<T, K extends keyof T> {
controls: FormControls<T, K>;
set(v: Partial<T>): void;
};

declare var formBuilder: {
group<T, K extends keyof T>(obj: T): FormGroup<T, K>;
}

const user: User = {
name: 'Quramy',
age: 32,
};

const formGroup = formBuilder.group(user);
controls.name.valueChanges.subscribe(x => {/** ... */});

name をboxingした FormControl(controls.name)は、name の型に従って、Observable<string> が返ってくる



まとめ


  • TypeScript 2.1.4では、動的にプロパティを扱う関数の型定義が書きやすくなった

  • コレクションやオブジェクトを操作する関数・ライブラリをより賢く記述できるようになった

  • Immutable.jsやRx、lodash 等がより改善される...かも?


おまけ

TypeScript 2.1ではLanguage Service関連の機能追加(quickfix, implementation jump等)も含まれるので、

エディタプラグイン屋さんにも嬉しい!


2016.12.08追記

2.1.4(2.1系安定版) のリリース案内 https://blogs.msdn.microsoft.com/typescript/2016/12/07/announcing-typescript-2-1/ では、keyofやMapped Typesにも触れられています。