🥳
TypeScript/指定した型のプロパティ名を取得する
TL;DR
type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never
}[keyof T]
または
type KeysMatching<T, V> = keyof {
[P in keyof T as T[P] extends V ? P : never]: any
} &
keyof T
👇
type Example = { foo: number; bar?: string; baz?: number }
type T1 = KeysMatching<Example, number> // 'foo' | 'baz'
type T2 = KeysMatching<Example, string> // 'bar'
解説
stack overflow にも解説があるので蛇足とはなりますが、一応解説をします。
基本的なこと
<T>
(Generic Types)
'foo' | 'bar'
(Union Types)
keyof
type Point = { x: number; y: number }
type P = keyof Point // 'x' | 'y'
Indexed Access Types (Lookup Types)
type Person = { age: number; name: string; alive: boolean }
type I1 = Person['age' | 'name'] // string | number
type I2 = Person[keyof Person] // string | number | boolean
in
type Fish = { swim: () => void }
type Bird = { fly: () => void }
const move = (pet: Fish | Bird): void => {
if ('swim' in pet) {
return pet.swim() // pet = Fish
}
return pet.fly() // pet = Bird
}
※ Mapped Types の in
とは区別して考えた方がよさそう
Mapped Types
Partial<T>
といった組み込みのユーティリティタイプは以下を参照のこと
{ [ P in K ] : T }
type T1 = { [P in 'x' | 'y']: number } // { x: number; y: number }
type T2 = { [P in 'x' | 'y']?: number } // { x?: number; y?: number }
type T3 = { readonly [P in 'x' | 'y']: number } // { readonly x: number; readonly y: number }
// type Record<K extends keyof any, T> = { [P in K]: T }
type T4 = Record<'x' | 'y', number> // { x: number; y: number }
{ [ P in K ] : T[P] }
type Point = { x: number; y: number; z: number }
type T1 = { [P in 'x' | 'y']: Point[P] } // { x: number; y: number }
// type Pick<T, K extends keyof T> = { [P in K]: T[P] }
type T2 = Pick<Point, 'x' | 'y'> // { x: number; y: number }
{ [ P in keyof T ] : T }
type Point = { x: number; y: number }
type T1 = { [P in keyof Point]: number } // { x: number; y: number }
type T21 = { [P in keyof Point]?: number } // { x?: number; y?: number }
type T22 = { [P in keyof Partial<Point>]: number } // { x?: number; y?: number }
type T23 = { [P in keyof Partial<Point>]-?: number } // { x: number; y: number }, -? で ? (optional) を無効にできる
type T31 = { readonly [P in keyof Point]: number } // { readonly x: number; readonly y: number }
type T32 = { [P in keyof Readonly<Point>]: number } // { x: number; y: number }, readonly にはならない
type PointReadonly = { readonly x: number; readonly y: number }
type T33 = { -readonly [P in keyof PointReadonly]: number } // { x: number; y: number }, -readonly で readonly を無効にできる
{ [ P in keyof T ] : T[P] }
type E1 = { foo: string; bar: string }
type T2 = { [P in keyof E1]: E1[P] } // { foo: string, bar: string }
type E2 = { foo: number; bar: number }
type T2 = { [P in keyof E1]: E2[P] } // { foo: number, bar: number }
type Point = { x: number; y: number }
type T3 = { [P in keyof Point]?: Point[P] } // { x?: number; y?: number }
// type Partial<T> = { [P in keyof T]?: T[P] }
type T4 = Partial<Point> // { x?: number; y?: number }
type PointPartial = { x?: number; y?: number }
type T5 = { [P in keyof PointPartial]-?: PointPartial[P] } // { x: number; y: number }
// type Required<T> = { [P in keyof T]-?: T[P] }
type T6 = Required<PointPartial> // { x: number; y: number }
type T7 = { readonly [P in keyof Point]: Point[P] } // { readonly x: number; readonly y: number }
// type Readonly<T> = { readonly [P in keyof T]: T[P] }
type T8 = Readonly<Point> // { readonly x: number; readonly y: number }
Key Remapping
type E1 = { foo: string; bar: string }
type T1 = { [P in keyof E1 as Uppercase<P>]: E1[P] } // { FOO: string; BAR: string }
type T2 = { [P in keyof E1 as Capitalize<P>]: E1[P] } // { Foo: string; Bar: string }
type T3 = { [P in keyof E1 as Exclude<P, 'foo'>]: E1[P] } // { bar: string }
type T4 = { [P in keyof E1 as Exclude<P, 'bar'>]: E1[P] } // { foo: string }
Template Literal Types を使うと、remapping の意味がわかりやすい
type Point = { x: number; y: number }
type T5 = { [P in keyof Point as `ex_${P}`]: Point[P] } // { ex_x: number; ex_y: number }
Conditional Types
// type NonNullable<T> = T extends null | undefined ? never : T
type T1 = NonNullable<string | number | undefined> // string | number
type T2 = NonNullable<string[] | null | undefined> // string[]
// type Extract<T, U> = T extends U ? T : never
type T3 = Exclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
type T4 = Exclude<'a' | 'b' | 'c', 'a' | 'b'> // 'c'
// Exclude<T, U> = T extends U ? never : T
type T5 = Extract<'a' | 'b' | 'c', 'a'> // 'a'
type T6 = Extract<'a' | 'b' | 'c', 'a' | 'b'> // 'a' | 'b'
KeysMatching
を読み解く
type Example = { foo: number; bar?: string; baz?: number }
// -? で optional を無効に
type K1<T> = {
[K in keyof T]-?: T[K]
}
type T1 = K1<Example> // { foo: number; bar: string; baz: number }
// Conditional Types で
// V 型のプロパティの値をプロパティ名に、そうでないプロパティを never に
type K2<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never
}
type T2 = K2<Example, 'string'> // { foo: 'foo'; bar: never; baz: 'baz' }
// Indexed Access Types で取り出す
type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never
}[keyof T]
type T3 = KeysMatching<Example, number> // 'foo' | 'baz'
または
type Example = { foo: number; bar?: string; baz?: number }
// Key Remapping で T[P] が V 型であるものを取り出す
type K1<T, V> = {
[P in keyof T as T[P] extends V ? P : never]: any
}
type E1 = K1<Example, number> // {foo: any, baz?: any}
// keyof でプロパティ名を取り出す
// ( プロパティ名しか参照しておらず : any 部分は使っていないため、これは任意の型でいいことがわかる )
type K2<T, V> = keyof {
[P in keyof T as T[P] extends V ? P : never]: any
}
type E2 = K2<Example, number> // 'foo' | 'baz'
// 複雑な推論を伴う場合だと K が keyof T だと推論できないことがある
const M2 = <T, K extends K2<T, number>>(obj: T, key: K) => {
return obj[key] // TS2536: Type 'K' cannot be used to index type 'T'.
}
// & keyof T で K が keyof T であることを明示する
type KeysMatching<T, V> = keyof {
[P in keyof T as T[P] extends V ? P : never]: never
} &
keyof T
type E3 = K3<Example, number> // 'foo' | 'baz'
const M3 = <T, K extends K3<T, number>>(obj: T, key: K) => {
return obj[key] // OK
}
Discussion