TypeScriptのinterfaceやtypeで、関数やオブジェクトの型を定義する

TypeScriptのinterfaceやtypeで、関数やオブジェクトの型を定義する際、どんな書き方をすればいいかちょこちょこ迷うのでメモ。

関数

numberを引数に取り、stringを返す関数の場合、以下のようになる。

// interfaceの場合
interface ExampleFunctionInterface {
  (value: number): string
}

// typeの場合
type ExampleFunctionType = (value: number) => string

旧ハンドブックにはinterfaceでの定義方法が記載されていたが、新しいハンドブックからはtypeでの定義方法しか見つけられなかった。

オブジェクト

インデックスシグネチャの型を指定してやればいい。

キーの型にはstring、number、Symbolが使える。

// interfaceの場合
interface StringKeyObjectInterface {
  [key: string]: unknown
}

// typeの場合
type StringKeyObjectType = {
  [key: string]: unknown
}

また、ユーティリティータイプの Record<Keys, Type> も使える。こちらのほうが可読性が高いと思う。

TypeScript: Documentation - Utility Types

type StringKeyRecord = Record<string, unknown>

ちなみに、 {}ban-types | TypeScript ESLint によると、任意のnullではないものを表すらしい。

github.com

Don't use `{}` as a type. `{}` actually means "any non-nullish value".
- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.
- If you want a type meaning "any value", you probably want `unknown` instead.
- If you want a type meaning "empty object", you probably want `Record<string, never>` instead.

Union型をキーとしたオブジェクト

ハンドブックの例にもあるが、stringやnumberからなるUnion型をRecordのキーに指定すると、キーに指定した値が過不足がある場合はコンパイルエラーにしてくれる。

type MyUnion = 'one' | 'two' | 'three'
const myUnionRecord: Record<MyUnion, number> = {
  one: 1, two: 2, three: 3,
}

インデックスシグネチャでも同様の指定ができるが、Mapped typeにする必要がある。

type MyUnion = 'one' | 'two' | 'three'
const myUnionIndexSignature: { [key in MyUnion]: number } = {
  one: 1, two: 2, three: 3,
}

複数のUnion型をキーにする場合、それぞれの型をUnion型にしてやればいい。

type MyUnion1 = 'one' | 'two' | 'three'
type MyUnion2 = 'four' | 'five' | 'six'

const myUnionRecord: Record<MyUnion1 | MyUnion2, number> = {
  one: 1, two: 2, three: 3,
  four: 4, five: 5, six: 6,
}

const myUnionIndexSignature: { [key in MyUnion1 | MyUnion2]: number } = {
  one: 1, two: 2, three: 3,
  four: 4, five: 5, six: 6,
}

キーの不足を許す場合、キーをオプショナルにすればいいが、Recordではキーが string | number | symbol である必要があり、undefinedを含むことができないため、宣言できない模様(方法があるかもしれないが、見つけられなかった)。

インデックスシグネチャの場合は、Mapped typeに ? を付けてやればいい。

type MyUnion = 'one' | 'two' | 'three'

// 以下はコンパイルエラー
// type MyUnionRecord = Record<MyUnion | undefined, number>

// こちらは問題なし
const myUnionIndexSignature: { [key in MyUnion]?: number } = {
  two: 2,
}

ただし、Union型自体にundefinedが含まれる場合、Recordと同様の理由でコンパイルエラーとなる模様。

type MyUnion = 'one' | 'two' | 'three' | undefined

// 以下はコンパイルエラー
// const myUnionIndexSignature: { [key in MyUnion]?: number } = ...

振り返り

interfaceよりtypeを使うことが多いからか、typeでの定義例は多いがinterfaceの例が少ない気がしたのでメモしておく。

また、今回参照したTypeScript ハンドブックには、他にコンストラクタの定義の方法なども記載されており、いい勉強になった。