All Articles

utility-typesライブラリのOmitの実装を見てみる

TypeScript の型を動的に作り出す機能はすごいですね。

OmitPickReadonly …etc の中の、 Omit にフォーカスした記事を書きましたが、今回は Omit の実装がとても気になってコードを読みましたので、備忘録にはなりますが、まとめます。

Omit のコードはとても短い

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

わずか 1 行で実現していますね。

雰囲気で読めそうな気はするものの正確に理解しないと、いざ書こうとしたときに書けないので、分解して読み解いていきます。

まず、Generics の T は外から指定された型であるのはわかります。

K extends keyof any は何でしょう?

K extends keyof any

extends は継承でよく使われるワードでイメージはつきますが keyof は何をしているのでしょう?

keyofは?

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

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

type UserKey = keyof User;

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

つまり、keyof型からプロパティ名を取り出す ものであるという理解ができました。

extends と組み合わせた例として、しっくりくる解説が、TypeScript Handbook を読む (6. Generics)に書かれていたのでサンプルを貼ります。

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

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // OK
getProperty(x, "m"); // エラー。'm' 型の引数は 'a' | 'b' | 'c' | 'd' 型に代入できない

K extends keyof TT型で定義されている、プロパティ名のみ受け付ける制約であることがわかります。

今回の Omit は、K extends keyof any となっていて、

any と書かれているので、どのようなプロパティ名も受け付けますよという意味ですね。

Pick と Exclude は何?

Pick<T, Exclude<keyof T, K>> と見慣れない記述が見つかりました。

ひとつずつ分解していきます。

まずは、Pick ついて見ていきたいと思います。

Pick

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

上で得た知識によると、 型引数は、 K extends keyof T と定義されているので、型引数の K は T の中で定義されているプロパティ名のみを受け付けることがわかります。

次に見慣れない、 [P in K] これは何でしょう?

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

のページの言葉を借りると、

(半)動的に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 }

|でつなげたプロパティ名を in でひとつずつ分解して型を作り出しているんですね。

Swift にもfor in 構文は存在するので、inの右側のものが展開され逐次実行するのはイメージがしやすかったです。

オフィシャルに、Pick の実例があったので、載せておきます。

interface Todo {
    title: string;
    description: string;
    completed: boolean;
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>;

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
};

もう一度 Pick を見てみると

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

K extends keyof T T 型で定義されているプロパティ名を K として

[P in K]: T[P] その K をひとつずつ展開して新しい型を作り出す

オフィシャルの例の結果とも一致するのでしっくり!

Exclude

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

オフィシャルのサンプルも貼り付けると

type T0 = Exclude<"a" | "b" | "c", "a">;  // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">;  // "c"
type T2 = Exclude<string | number | (() => void), Function>;  // string | number
Extract<T,U> #

T が U なら never, それ以外は T の型を返す

React を TypeScript で書く TypeScript 編 3 の解説がわかりやすかったです。

never は何のためにあるのかオフィシャル見たときに理解できなかったのですが、こういう使い方があるんですね。

Omit の実装コードをもう一度確認する

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Exclude<keyof T, K> は、 keyof T で型からプロパティ名に変換してその中から、Kのプロパティ名を取り除いています。

具体例を使って見ていきます。

T のキーが a b c d であって、 K がa b とした例にします。

Omit の機能としての期待値は、c d です。

Exclude<keyof T, K> により c d となります。

Pick<T, Exclude<keyof T, K>>

Pick<"a" | "b" | "c" | "d", "c", "d"> は

c d となります。

期待値とも一致しました。

まとめ

型まわりの書き方は慣れていないのでわずか数行でも読み応えがあるものでしたが、徐々に慣れていきたいと思います。

ひとつずつ丁寧に理解していきたいと思います。