この記事の内容は 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
を使ったケースのほうが正規化されており望ましいように思える。