🥳

TypeScript/指定した型のプロパティ名を取得する

2021/06/14に公開

TL;DR

https://stackoverflow.com/a/54520829

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)

https://www.typescriptlang.org/docs/handbook/2/generics.html

'foo' | 'bar' (Union Types)

https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html

keyof

https://www.typescriptlang.org/docs/handbook/2/keyof-types.html

type Point = { x: number; y: number }
type P = keyof Point // 'x' | 'y'

Indexed Access Types (Lookup Types)

https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html

type Person = { age: number; name: string; alive: boolean }
type I1 = Person['age' | 'name'] // string | number
type I2 = Person[keyof Person] // string | number | boolean

in

https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-the-in-operator

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

https://www.typescriptlang.org/docs/handbook/2/mapped-types.html

Partial<T> といった組み込みのユーティリティタイプは以下を参照のこと
https://www.typescriptlang.org/docs/handbook/utility-types.html
https://github.com/microsoft/TypeScript/blob/a875635ea7e3c37fb44492c30571f91ce65a635d/lib/lib.es5.d.ts#L1468

{ [ 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

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html

// 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