keyof を使うと、オブジェクトのすべてのキーを、文字列リテラル型の合併として取得できます。
あるtypeのキーを合併の型として、新たにtype宣言や型の制約をする場合に使用。
基本
type Test = {
a: number,
b: string,
c: boolean
}
//testKeyofはTestのキー「'a' | 'b' |'c'」の型になる
type testKeyof = keyof Test
let a:testKeyof = 'a' //OK
let b:testKeyof = 'b' //OK
let c:testKeyof = 'c' //OK
let d:testKeyof = 'd' //エラー
入れ子
オブジェクトが入れ子の場合。
type Test = {
a: {
b: {
b2: string
}
c: {
c2: number
}
}
}
type testKeyof = keyof Test //型は「a」
let a:testKeyof = 'a' //OK
type testKeyof2 = keyof Test['a'] //型は「'b' | 'c'」
let b:testKeyof2 = 'b' //OK
let c:testKeyof2 = 'c' //OK
type testKeyof3 = keyof Test['a']['b'] //型は「'b2'」
let b2:testKeyof3 = 'b2' //OK
type testKeyof4 = keyof Test['a']['c'] //型は「'c2'」
let c2:testKeyof4 = 'c2' //OK
応用
type Test = {
a: number
b: string
c: boolean
}
function sam<O extends object, K extends keyof O>(o: O, k: K): O[K] {
return o[k]
}
ジェネリック型のOはobjectの型でなければいけません。
Oを型Testと想定してみます。すると「keyof O」はTestの全てのキーの合併「’a’ | ‘b’ |’c’」になります。
これでKは型「’a’ | ‘b’ |’c’」のサブタイプである事がわかります。
仮にKが「a」であるならば戻り値O[K]はTest[‘a’]であり戻り値は「number」になります。
例1
function sam<O extends object, K extends keyof O>(o: O, k: K): O[K] {
return o[k]
}
type Test = {
id: number
name: string
options?: {
type: 'men' | 'women'
timestamp: Date
}
}
let TestLog: Test = {
id: 10,
name: 'tanaka',
options: {
type: 'men',
timestamp: new Date
}
}
let testOptions = sam(TestLog, 'options')
console.log(testOptions) //{ type: 'men', timestamp: 2020-06-22T19:22:33.201Z }
ジェネリクスの型はtypescriptが推論します。
sam関数の第1引数の「o」にオブジェクト「TestLog」を渡したので、ジェネリクス型「O」をTest型と推論します。
その場合「keyof O」は「’id’|’name’|’options’」になり、ジェネリクスの型Kは「’id’|’name’|’options’」のサブタイプになります。
sam関数の第2引数の「k」に文字列「’options’」を渡したので、戻り値o[k]はTestLog[‘options’]の示す値になります。
例2
type Test = {
id: number
name: string
options: {
type: 'men' | 'women'
timestamp: Date
}[]
}
let TestLog: Test = {
id: 10,
name: 'tanaka',
options: [{
type: 'men',
timestamp: new Date
}]
}
type Get = {
//①
<O extends object, K1 extends keyof O>(o: O, k1: K1): O[K1]
//②
<O extends object, K1 extends keyof O, K2 extends keyof O[K1]>
(o: O, k1: K1, k2: K2): O[K1][K2]
//③
<O extends object, K1 extends keyof O, K2 extends keyof O[K1], K3 extends keyof O[K1][K2]>
(o: O, k1: K1, k2: K2, k3: K3): O[K1][K2][K3]
}
let get: Get = (object: any, ...keys: string[]) => {
let result = object
keys.forEach(k => result = result[k])
return result
}
//④
console.log(get(TestLog, 'options', 0, 'type')) //men
Get 型(オーバーロードされた関数シグネチャを宣言)
Get 型の関数を作成するので type Get の宣言です。
関数呼び出し時にTest型のオブジェクト「TestLog」渡すので、その想定で説明していきます。
1つのキー、2つのキー、3つのキーでそれぞれ 呼び出す場合のための3つのケースを指定します。
type Get = {
//① ④で「TestLog」渡されるので「O」はTest型のオブジェ「TestLog」、
//「keyof O」は「'id'|'name'|'options'」、
//④で「'options'」が渡されるので O[K1] は、TestLog['options']になる。
<O extends object, K1 extends keyof O>(o: O, k1: K1): O[K1]
//② O[K1] は TestLog['options'] の値、[{type: 'men', timestamp: 2020-06-23T16:14:36.967Z}]
//なので O[K1] は配列になる。「keyof 配列」は「number」になるで「K2 extends number」になる。
//④で「0」が渡されているので O[K1][K2] は TestLog['options'][0] になる。
<O extends object, K1 extends keyof O, K2 extends keyof O[K1]>
(o: O, k1: K1, k2: K2): O[K1][K2]
//③ O[K1][K2] は TestLog['options'][0] の値、{type: 'men', timestamp: 2020-06-23T16:14:36.967Z}
//なので「keyof O[K1][K2]」は「'type'|'timestamp'」になる。
//④で「'type'」が渡されているので O[K1][K2][K3] は TestLog['options'][0]['type'] になる。
<O extends object, K1 extends keyof O, K2 extends keyof O[K1], K3 extends keyof O[K1][K2]>
(o: O, k1: K1, k2: K2, k3: K3): O[K1][K2][K3]
}
②の箇所の 「「keyof 配列」は「number」になる」の説明。
※ この箇所はもっと的確な説明があるかと思います。
type a = []
type b = keyof a
let obj1:b = 0 //OK
let obj2:b = 100 //OK
let obj3:b = "" //エラー
let obj4:b = {} //エラー
let obj5:b = true //エラー
関数/関数呼び出し
Get 型の関数を作成し、呼び出します。
let get: Get = (object: any, ...keys: string[]) => { //第1引数以後は可変長にしておく
let result = object
keys.forEach(k => result = result[k])
return result
}
//④
console.log(get(TestLog, 'options', 0, 'type')) //men
//第2引数渡しまでのそれぞれの出力
console.log(get(TestLog, 'options')) //[{type: 'men', timestamp: 2020-06-23T19:04:04.449Z}]
console.log(get(TestLog, 'options', 0)) //{type: 'men', timestamp: 2020-06-23T19:04:04.449Z}
まとめたもの
type Test = {
id: number
name: string
options: {
type: 'men' | 'women'
timestamp: Date
}[]
}
let TestLog: Test = {
id: 10,
name: 'tanaka',
options: [{
type: 'men',
timestamp: new Date
}]
}
type Get = {
//① ④で「TestLog」渡されるので「O」はTest型のオブジェ「TestLog」、
//「keyof O」は「'id'|'name'|'options'」、
//④で「'options'」が渡されるので O[K1] は、TestLog['options']になる。
<O extends object, K1 extends keyof O>(o: O, k1: K1): O[K1]
//② O[K1] は TestLog['options'] の値、[{type: 'men', timestamp: 2020-06-23T16:14:36.967Z}]
//なので O[K1] は配列になる。「keyo 配列」は「number」になるで「K2 extends number」になる。
//④で「0」が渡されているので O[K1][K2] は TestLog['options'][0] になる。
<O extends object, K1 extends keyof O, K2 extends keyof O[K1]>
(o: O, k1: K1, k2: K2): O[K1][K2]
//③ O[K1][K2] は TestLog['options'][0] の値、{type: 'men', timestamp: 2020-06-23T16:14:36.967Z}
//なので「keyof O[K1][K2]」は「'type'|'timestamp'」になる。
//④で「'type'」が渡されているので O[K1][K2][K3] は TestLog['options'][0]['type'] になる。
<O extends object, K1 extends keyof O, K2 extends keyof O[K1], K3 extends keyof O[K1][K2]>
(o: O, k1: K1, k2: K2, k3: K3): O[K1][K2][K3]
}
let get: Get = (object: any, ...keys: string[]) => { //第1引数以後は可変長にしておく
let result = object
keys.forEach(k => result = result[k])
return result
}
//④
console.log(get(TestLog, 'options', 0, 'type')) //men
//第2引数渡しまでのそれぞれの出力
console.log(get(TestLog, 'options')) //[{type: 'men', timestamp: 2020-06-23T19:04:04.449Z}]
console.log(get(TestLog, 'options', 0)) //{type: 'men', timestamp: 2020-06-23T19:04:04.449Z}