この記事の内容は TypeScript のv4.1.3で、compilerOptions.noUncheckedIndexedAccessを有効にした状態で動作確認している。
参考: zenn.dev
恒等関数(Identity Function)とは、渡されたものを返す関数。
function identity<T>(arg: T) { return arg; } const x = identity(1); // const x: 1 const y = identity(() => 1); // const y: () => 1
引数をそのまま返しているため当然だが、値は変わらない。
このままだと何の意味もないが、extendsキーワードを使って型に制約を与えることができる。
例えば以下のidentityには、ReturnNumberかそのサブタイプしか渡せない。
type ReturnNumber = () => number; const identity = <T extends ReturnNumber>(arg: T) => arg; identity(() => 1); // Ok identity(() => 'a'); // Error identity((x: number) => x); // Error
これだけでは変数名: ReturnNumberのようにアノテートするのと、変わらないように見える。
だが以下のケースでは、xとyで型が異なっている。
type ReturnNumber = () => number; const identity = <T extends ReturnNumber>(arg: T) => arg; const x = identity((): 1 => 1); const y: ReturnNumber = (): 1 => 1; type Foo = typeof x; // () => 1 type Bar = typeof y; // () => number
yにはReturnNumberとアノテートしているので、yの型はReturnNumber(つまり() => number)になる。
だがxに対してはアノテートしていないので、identityに渡した(): 1 => 1がそのまま返ってきて代入されるので、() => 1になる。
このように恒等関数とextendsキーワードを活用することで、値に制限を加えつつ、本来の型を保つことができる。
このテクニックを使うことで、従来は難しかった表現が可能になる。
例えば以下のxはObjという制約を満たしている。
それでいてidentityに渡されたオブジェクトリテラルの型がそのまま保持されるので、keyof typeof xで具体的な情報を取れるし、プロパティへのアクセスも適切に機能する。
type Value = number; type Obj = Record<string, Value>; const identity = <T extends Obj>(arg: T) => arg; const x = identity({ one: 1, two: 2, three: 3, }); type Foo = keyof typeof x; // "one" | "two" | "three" x.one; // number x.foo; // Error
そしてObjの制約に違反するようなフィールドを追加すると、TypeScript がエラーを出す。
const x = identity({ one: 1, two: 2, three: 3, four: '4', // Error });
同様のことを恒等関数を使わずに実現しようとすると、かなり難しくなる。
まず、xに対して何もアノテートをつけないと、当然のように何の制約も与えられない。
const x = { one: 1, two: 2, three: 3, four: '4', // Error にならない };
なので、Objでアノテートしてみる。
そうすると、four: '4'のようなフィールドを加えようとした時に、TypeScript がエラーを出してくれるようになる。
しかし今度は、Fooから詳細な情報が失われ、stringになってしまった。
また、全てのプロパティへのアクセスがnumber | undefinedになってしまった。
これは先程のidentityを使ったケースに比べて、明らかに使い勝手が悪くなっている。
type Value = number; type Obj = Record<string, Value>; const x: Obj = { one: 1, two: 2, three: 3, }; type Foo = keyof typeof x; // string x.one; // number | undefined x.foo; // number | undefined
以下のようにKeyを用意することで、同等の使い勝手を取り戻せる。
type Key = "one" | "two" | "three"; type Value = number; type Obj = Record<Key, Value>; const x: Obj = { one: 1, two: 2, three: 3, }; type Foo = keyof typeof x; // "one" | "two" | "three" x.one; // number x.foo; // Error
だがこの場合、フィールドを追加する度にKeyとxの両方に記述しないといけない。
@@ -1,4 +1,4 @@ -type Key = "one" | "two" | "three"; +type Key = "one" | "two" | "three" | "four"; type Value = number; type Obj = Record<Key, Value>; @@ -6,4 +6,5 @@ one: 1, two: 2, three: 3, + four: 4, };
どちらか一方にだけ記述するとエラーになるため記述し忘れることはないだろうが、手間であることには変わりない。
同じ情報を二箇所で管理することになってしまっているわけで、identityを使ったケースのほうが正規化されており望ましいように思える。