この投稿では、TypeScriptの次のような問題と理想とその理想を実現する手法を紹介します。
- 問題点:
keyof
をインデックスシグネチャを持つインターフェイスに対して使うと、string | number
になってしまう。 - 理想:
string | number
じゃなくて、インデックスシグネチャ以外の既知キーを知りたい。
keyof
は良い
TypeScriptにはkeyof
というものがあり、インターフェイスが持つキーを調べることができます:
interface Foo {
a: number
b: number
c: number
}
type FooKeys = keyof Foo
//=> "a" | "b" | "c"
オブジェクトのキーを値で扱うときに、タイポに気づきやすく安全になったり、コード補完も効いたりして便利です:
let fooKeys: keyof Foo
fooKeys = 'a' // OK
fooKeys = 'b' // OK
fooKeys = 'c' // OK
fooKeys = 'd' // Error: dなんて無いよと教えてもらえる!
keyof
をインデックスシグネチャに使うと良さが半減
ところが、インデックスシグネチャを持つインターフェイスに対して、keyof
をすると、string | number
のようなざっくりとした型になってしまうので、:
interface Bar {
a: number
b: number
c: number
[key: string]: any // インデックスシグネチャ
[key: number]: any // インデックスシグネチャ
}
type BarKeys = keyof Bar
//=> string | number
前述したような安心感が半減します:
let barKeys: keyof Bar //=> string | number
barKeys = 'a' // OK
barKeys = 'b' // OK
barKeys = 'c' // OK
barKeys = 'd' // OK (´・ω・`)
barKeys = 1 // OK (´・ω・`)
Bar
インターフェイスは「キーはstring
か、number
だったら何でもいいよ」と定義しています。
なので、上のコードのように未知のキーを扱えるのは、論理的には正しいです。
既知のキーだけ取り出すKnownKeyOf<T>
を作る
とは言っても、実用上、既知のキーだけを知りたいことがあります。
理想的にはこうしたい:
let barKnownKeys: KnownKeyOf<Bar> //=> "a" | "b" | "c"
barKnownKeys = 'a' // OK
barKnownKeys = 'b' // OK
barKnownKeys = 'c' // OK
barKnownKeys = 'd' // Error: dなんて知らないよと教えてもらえる 😊
barKnownKeys = 1 // Error: 1なんて知らないよと教えてもらえる 😊
これを叶えてくれるKnownKeyOf<T>
は次のような実装になります:
type KnownKeyOf<T> = {
[K in keyof T]: string extends K ? never :
number extends K ? never : K
} extends { [_ in keyof T]: infer X }
? X
: never
KnownKeyOf<T>
の解説
ちょっと込み入ったコードになるので説明します。
KnownKeyOf<T>
は2つのパートに分けられます。
1つ目はこの部分:
{
[K in keyof T]: string extends K ? never :
number extends K ? never : K
}
これは、T
インターフェイスを、{ キー名: 値の型 }
から{ キー名: キー名 }
に変換する部分です。ただ、「値の型」から「キー名」に変換するとき、細工があって、「キー名」がリテラル型じゃないのときだけ、{ キー名: never }
にするようになっています。つまり、インデックスシグネチャのキーだけnever
で目印をつけているわけです。例えば、Bar
をこの部分に当てはめると、次のような構造のインターフェイスが導出されます:
type Part1 = {
[K in keyof Bar]: string extends K ? never :
number extends K ? never : K
}
// ↓
type Part1 = {
[x: string]: never; // インデックスシグネチャだけ
[x: number]: never; // neverになる!
a: "a";
b: "b";
c: "c";
}
Part1
に続く、2つ目のパートはここ:
Part1 extends { [_ in keyof T]: infer X }
? X
: never
これは、Part1
でnever
の目印をつけたキーを除去する部分です。infer X
は、{ キー名①: キー名② | never }
の「キー名②」部分をX
に代入します。この代入の段階で、never
が自動的にはじかれ、既知のキー名だけに絞り込まれます。
備考
複雑なインターフェイスでも期待通り動くことは確認済みです:
type Complex = {
a: number
readonly b: string
readonly c?: boolean
d: number | string
[key: string]: any
}
type KnownKeyOfComplex = KnownKeyOf<Complex>
//=> type KnownKeyOfComplex = "a" | "b" | "c" | "d"
type ComputedKeys = {
[key in 'a' | 'b']: string
} & {
[key in 1 | 2 ]: string
} & {
[key: string]: string
[key: number]: string
}
type KnownKeyOfComputedKeys = KnownKeyOf<ComputedKeys>
//=> type KnownKeyOfComputedKeys = "a" | "b" | 1 | 2