TypeScript で、メソッドを含めない型を作る

TypeScript

小ネタ。
TypeScript で、下記コードのような Interface からメソッドを抜いた型を作りたいとする。

type.ts

declare class Foo {
  id: string
  timeout: number
  start: () => void
  stop: () => void
  on: (event: Event) => void
}

keyof T から関数の型を持つキーを取っ払ったユニオン型を作り、それを使って Pick<T, K extends keyof T> すればいける。

type.ts

type ExcludeMethods<T> = Pick<
  T,
  {
    [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K
  }[keyof T]
>

やること

まず Mapped Types と Conditional Types を使って、関数の型であれば never 型、それ以外ならキーの型を作る。

type.ts

type ExcludeMethods<T> = { [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K }

この時点では、キーとキー名の型しか出ない。

typescript:title=型

type ExcludeMethodsOfFoo = ExcludeMethods<Foo>
// {
//  id: "id";
//  timeout: "timeout";
//  start: never;
//  stop: never;
//  on: never;
// }

次に Lookup Types で取り出す。ここではユニオン型が使えるので、[keyof T] を指定する。
ユニオン型は never 型を省略するので、[keyof T] からメソッドを持つキーが省略され、"id" | "timeout" が取れる。

type.ts

type ExcludeMethods<T> = { [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K }[keyof T]
type ExcludeMethodsOfFoo = ExcludeMethods<Foo>
// { id: "id" } "id" が入る
// { start: never } never なので省略される
// "id" | "timeout"

"id" | "timeout" のユニオン型になったので、冒頭で書いたように Pick で取れる。
最終的にはこう。

type.ts

type ExcludeMethods<T> = Pick<
  T,
  {
    [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K
  }[keyof T]
>

const foo: ExcludeMethods<Foo> = {
  id: 'foo',
  // timeout: number が存在しないのでエラー
  start: () => {}, // start は Pick されていないのでエラー
}