角待ちは対空

おもむろガウェイン

TypeScript2.1.4 で導入された `keyof` キーワードと `in` キーワード、そして Lookup Types と Mapped Types

keyof キーワード

key とオブジェクトを受け取りプロパティの値を取り出す関数を考えます。

function getProp(obj: {}, key: string) {
    return obj[key];
}

この関数使って変数を宣言すると型推論では返り値は any になってしまいます。

const urara = {
    age: 15,
    name: "chiya",
};

const a = getProp(urara, 'age'); // any
const n = getProp(urara, 'name'); // any

もうちょい頑張って型付けしたいと思ったら obj の型を絞るしかなさそうです。

interface Urara {
    age: number;
    name: string;
}

では key の型は?となると今までは素朴に String Literal type で羅列するしかありませんでした。

type UraraKeys = 'age' | 'name';

ですが keyof キーワードが追加されたことにより、 UraraKeys の定義が少し楽になります。

type UraraKeys = keyof Urara;

めでたい。動作は想像通り key の Union Type が返ります。

これらを使うと getProp() はこんな感じになります。

function getProp(obj: Urara, key: UraraKeys) {
    return obj[key];
}

interface Urara {
    age: number;
    name: string;
}

type UraraKeys = keyof Urara;

const urara = {
    age: 15,
    name: "chiya",
};

const a = getProp(urara, 'age'); // string | number
const n = getProp(urara, 'name'); // string | number

Lookup Types

keyof の登場によって多少マシになったとは言えまだイマイチです。具体的には

  • obj の型を具体的にする必要がある
  • いちいち key の型を宣言しなくてはならない
  • 型推論では string | number になってしまう

微妙ですね。

ですがこれも TS2.1.4 では解決されており

function getProp<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}

と書くと T[K] が返るようになります。 これにより

const a = getProp(urara, 'age'); // number
const n = getProp(urara, 'name'); // string

とキャストする必要がなくなります。めでたい。

実際使うの?

実例としては Object.entries() (lib.es2017.object.d.ts) の定義で使われています。

Mapped Types

Object.freeze() のラッパー関数を考えます。

interface Urara {
    age: number;
    name: string;
}

interface FrozenUrara {
    readonly age: number;
    readonly name: string;
}

function freezeUrara(u: Urara): FrozenUrara {
    return Object.freeze(u);
}

const fu = freezeUrara({ name: 'chiya', age: 15 });

fu.age = 12; // error

freezeUraraUrara 型を受け取って FrozenUrara を返す関数です。 FrozenUraraUrara の書くプロパティに readonly を付けただけの型。

これ毎回2つインターフェースを用意するはだるいですよね。今までは必要だったのですがこれも TS2.1.4 では解決してます。 どんなふうにかというと既に Object.freeze() 自体がいい感じになっているのでそれを見ると良さそうです(なのでラッパーとか作らなくてよかった)。

freeze<T>(o: T): Readonly<T>;

Readonly とはなにかと言うと定義はこんなふうになってます。

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

in キーワードが登場しました。

これは mapped type を扱うためのキーワードでまぁ想像通り in の後に置かれた Union Type から一つづつ取り出し map していきます。 この場合は keyof T から一つづつ取り出し readonly の付いた T[P] を返します。これでいちいちインターフェースを2つ作る必要がなくなりました。

Readonly みたいな型は他にも定義されていて

/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

/**
 * From T pick a set of properties K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends string, T> = {
    [P in K]: T;
};

の3つがTSの本体に入ってる。

あとは

/**
 * Make all properties in T nullable
 */
type Nullable<T> = {
    [P in keyof T]: T[P] | null;
};

/**
 * Turn all properties of T into strings
 */
type Stringify<T> = {
    [P in keyof T]: string;
};

みたいなのが便利なので必要なら自分で定義してねって提案されています。

実際使うの?

react の setState とか lodash の型定義が賢くなると言われてますが、実際アップデートされたかは見てないです。