DefinitelyTypedに対して既存のライブラリの型定義をPRで送ってマージされた話

## はじめに

普段、[camelcase-keys](https://github.com/sindresorhus/camelcase-keys)や[snakecase-keys](https://github.com/bendrucker/snakecase-keys)などを利用していたが、姉妹ライブラリとして[kebabcase-keys](https://github.com/mattiloh/kebabcase-keys)があるのを知った。このライブラリではTypeScriptの型定義（index.d.ts）がなかった。そこで、

https://qiita.com/arx8/items/b7f68dc971914ac49b53

という記事を参考に、OSSに貢献！という事でkebabcase-keysの型定義を追加するPRを上げてマージされたので、その型定義の作業とそこで利用した記法などについて備忘録を残しておく。

マージされて公開されて型定義は以下。

https://www.npmjs.com/package/@types/kebabcase-keys

## [kebabcase-keys](https://github.com/mattiloh/kebabcase-keys)

ライブラリの機能としては単純で、JSONのキーを[lodash.kebabCase](https://lodash.com/docs/4.17.15#kebabCase)を利用してkebab-caseに変換する、というもの。姉妹ライブラリに[camelcase-keys](https://github.com/sindresorhus/camelcase-keys)や[snakecase-keys](https://github.com/bendrucker/snakecase-keys)があり、以前、express-openapi-validatorのmiddlewareにカスタムで[このケース変換ライブラリを利用した](https://note.com/shift_tech/n/n66f43685f2f9#case%20%E5%A4%89%E6%8F%9B%E3%81%AE%20middleware%20%E3%83%AD%E3%82%B8%E3%83%83%E3%82%AF%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)事があった。

## 型定義を行う

[camelcase-keys](https://github.com/sindresorhus/camelcase-keys)や[snakecase-keys](https://github.com/bendrucker/snakecase-keys)の型定義を参考に、もっと楽して定義する事はできないか？という観点で今回は型定義を行っていった。

### 型定義を始める前の事前準備

[DefinitelyTypedの手順](https://github.com/DefinitelyTyped/DefinitelyTyped#how-can-i-contribute)に書かれている通り、まずはPRを上げる前に、自分のローカル環境で型が利用できるか？を確認する必要がある。そのためにはローカルの型定義を読み込ませる設定が必要になるので、まずはその事前準備を行う。

#### 型定義をしたいライブラリを依存に追加する

まずは、型定義を追加したいNode.jsのライブラリを依存に追加する。今回は[kebabcase-keys](https://github.com/mattiloh/kebabcase-keys)というライブラリの型定義を追加したいので、それを依存に追加する。

```console
$ yarn add kebabcase-keys
```

#### tsconfig.jsonの設定を変更し、ローカルの型定義を参照できるようにする

tsconfig.jsonの[typeRoots](https://www.typescriptlang.org/tsconfig#typeRoots)の設定にローカルのパスを追加する。

```json:tsconfig.json
{
	...
	"compilerOptions": {
		...
		"typeRoots": ["./srv/types", "./node_modules/@types"]
	},
	...
}
```

これで自動で参照される`node_modules/@types`以外からも型定義を参照できるようになる。

最後に、`./types/kebabcase-keys/index.d.ts` を作成して準備は完了になる（この`index.d.ts`が型定義のファイル）。

```ts:types/kebabcase-keys/index.d.ts
declare module 'kebabcase-keys' {}
```

※ちなみに、型定義がない状態で`tsc`コマンドでコンパイルを行っても、以下のようにエラーになる。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/e7d71439-5c1d-a499-5e8d-5cc781b19cc4.png)

### Step1　Genericsで引数のJSONを元に型定義をする

まずは、Genericsを利用して引数に渡された値を元に、キーのケース変換を行い戻り値の型を定義する。定義としては以下のようにした。

```ts:srv/types/kebabcase-keys/index.d.ts
declare module 'kebabcase-keys' {
	import { JsonObject, KebabCase } from 'type-fest';

	type Options = {
		...
		readonly deep?: boolean;
		...
		readonly exclude?: ReadonlyArray<string | RegExp>;
	};

	type KebabCasedProperties<T> = T extends readonly JsonObject[]
		? {
				[Key in keyof T]: KebabCasedProperties<T[Key]>;
		  }
		: T extends JsonObject
		? {
				[Key in keyof T as KebabCase<Key>]: T[Key] extends JsonObject | JsonObject[]
					? KebabCasedProperties<T[Key]>
					: T[Key];
		  }
		: T;

	declare function kebabcaseKeys<T extends JsonObject | JsonObject[]>(
		input: T,
		options?: Options
	): KebabCasedProperties<T>;

	export = kebabcaseKeys;
}
```

上記のようにする事で、以下のようなコードを安全に実装できる。具体的には、以下の画像のようにJsonのキーがケバブケースに変換されて、VS Codeからは変換後のキー名でサジェストが出るようになる。

| 引数 |  |  |
|:-:|:-:|:-:|
| json | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/dadf1580-4b10-3173-b619-3858c8700715.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/82103683-a2d9-f021-e941-60f8cf898832.png) |
| json[] | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/9333625a-3797-ad9b-e2f8-75bf2dbc757a.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/f73e2007-d620-6ab2-4ab2-b877c957a86c.png) |

```ts:srv/index.ts
// 上記の画像の実装例
import kebabcaseKeys from 'kebabcase-keys';

const result = kebabcaseKeys({
	foo_bar: 'baz',
	nested: { fooBaz: 'bar', arrayKey: ['123'], arrayJson: [{ testTest: 'test' }] }
});
console.log(result['foo-bar']);

const result2 = kebabcaseKeys([
	{ foo_bar: 'baz' },
	{ nested: { fooBaz: 'bar', arrayKey: ['123'], arrayJson: [{ testTest: 'test' }] } }
]);
console.log(result2[0]?.['foo-bar']);
```

そして、引数の方も以下のように、`JsonObject`か`JsonObject[]`を要求するようになるので、文字列の配列など`kebabcase-keys`の実行時にエラーになるものをあらかじめ受け付けないようにできる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/0a8b689a-2383-6df0-8c41-95196d832be3.png)

ここまでのコードは以下。

https://github.com/yutak23/type-kebabcase-keys/commit/118304123e1deddb95ccaca2a0af68494aba05d2

※ちなみに、[snakecase-keys](https://github.com/bendrucker/snakecase-keys/blob/master/index.d.ts)では`T extends Record<string, any> | readonly any[]`で型を定義していたが、これだと`Array<string>`が許容されるので、以下のようにコンパイルエラーにはならないが、実行時にエラーになってしまう。

```ts:srv/index.ts
// コンパイルエラーにはならないが、コンパイル後の実行時にエラーになる実装
import kebabcaseKeys from 'kebabcase-keys';

const result = kebabcaseKeys(['aa']); // ← コンパイルエラーにならず、コンパイル後のNode.jsの実行時にエラーになる
console.log(result);
```
```console:エラーの内容
$ node build/app.js
/home/study/workspace/ts-node-oidc/node_modules/kebabcase-keys/node_modules/map-obj/index.js:21
        isSeen.set(object, options.target);
               ^

TypeError: Invalid value used as weak map key
    at WeakMap.set (<anonymous>)
    at mapObject (/home/study/workspace/ts-node-oidc/node_modules/kebabcase-keys/node_modules/map-obj/index.js:21:9)
    at kebabCaseConvert (/home/study/workspace/ts-node-oidc/node_modules/kebabcase-keys/index.js:16:9)
    at /home/study/workspace/ts-node-oidc/node_modules/kebabcase-keys/index.js:37:40
    at Array.map (<anonymous>)
    at module.exports (/home/study/workspace/ts-node-oidc/node_modules/kebabcase-keys/index.js:37:29)
    at file:///home/study/workspace/ts-node-oidc/build/app.js:4:16
    at ModuleJob.run (node:internal/modules/esm/module_job:193:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:530:24)
```

※後に分かったが、type-festをDefinitelyTypedの依存に加えるのはNGであると[過去の議論](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59806#pullrequestreview-942584759)の中で決まったようで、type-festを利用せずに[TypeScript convert generic object from snake to camel case](https://stackoverflow.com/questions/60269936/typescript-convert-generic-object-from-snake-to-camel-case)を参考に、テンプレートリテラルを利用した型定義に変更した（コード全体は[ここ](https://github.com/yutak23/type-kebabcase-keys/commit/a47af98072151e327a58580755761816640f3399)）。

#### 型定義の方法について

- `kebabcaseKeys<T extends JsonObject | JsonObject[]>`
genericで第1引数に渡されるものが、JsonObject or JsonObject[] を継承している事が保障される。JsonObjectの定義については[type-festの定義](https://github.com/sindresorhus/type-fest#json)を参照。
実装方法としては[型引数に制約をつける](https://typescriptbook.jp/reference/generics/type-parameter-constraint#%E5%9E%8B%E5%BC%95%E6%95%B0%E3%81%AB%E5%88%B6%E7%B4%84%E3%82%92%E3%81%A4%E3%81%91%E3%82%8B)を利用している。

- `T extends readonly JsonObject[] ? A : B`
これは[Conditional Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html)で、TがJsonObject[]を継承する場合にはAを、そうでない場合はBを、という意味。

- `{ [Key in keyof T]: KebabCasedProperties<T[Key], Deep>; }`
これは`T extends readonly JsonObject[]`の条件に合致する時の型定義だが、実装としては[Mapped types on tuples and arrays](https://www.typescriptlang.org/docs/handbook/2/keyof-types.html)を利用している。もし、`{ [Key in keyof T]: T[Key] }`の場合は、そのままtuplesの中身が型として定義される（以下の画像の通り）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/0c3f7ee7-195e-4be1-0cdd-19cec5d46ca0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/58b69150-205f-c8c5-3990-3eb56664ef7f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/5ba72477-eb69-652d-ed30-c2fdc0d0c297.png)
だが、`T[Key]`を`KebabCasedProperties<T[Key], Deep>`とKebabCasedPropertiesタイプに変更しているので、`T extends CustomJsonObject`の三項演算子の`?`以降の型定義が適用される。
 
- `[Key in keyof T as KebabCase<Key>]`
`keyof T`は[Keyof Type Operator](https://www.typescriptlang.org/docs/handbook/2/keyof-types.html)で、オブジェクトのキーの組合わせを生成するが、それを[Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html)と併用する事でオブジェクトTのキーをプロパティのキーにできる。そこに[Type Assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions)で、ある型（今回は[KebabCase](https://github.com/sindresorhus/type-fest/blob/main/source/kebab-case.d.ts)）にアサーションする（kebab-caseにしたものをプロパティのキーにする）、という事をしてTのオブジェクトのキーをkebab-caseにした新しい型を定義している。

- `T[Key] extends JsonObject | JsonObject[] ? A : B`
JSONのvalueがJSONである事もあり得るので、再帰的に型をKebabCasedPropertiesに変更するような実装をしている。

※keyofは

```
Note that in this example, M is string | number — this is because JavaScript object keys are always coerced to a string, so obj[0] is always the same as obj["0"].
```

と書かれている通り、stringとは限らないので、以下のように分岐が必要になるので注意。

```ts
	type KebabCase<S extends string | number | symbol> = S extends number
		? `${S}`
		: S extends symbol
		? never
		: S extends string
		? AnyCaseToKebab<S>
		: S;
```

- 参考：https://www.typescriptlang.org/docs/handbook/2/keyof-types.html#the-keyof-type-operator
- 参考：https://ymizushi.hateblo.jp/entry/2022/06/20/232918
- 参考：[サバイバルTypeScript　keyof型演算子](https://typescriptbook.jp/reference/type-reuse/keyof-type-operator)

### Step2　JSONのvalueに`object`を許容する

[type-fest](https://github.com/sindresorhus/type-fest#json)の`JsonObject`だとkey-valueのvalueに[Date](https://typescriptbook.jp/reference/builtin-api/date)などのオブジェクトを指定した場合、以下のようにコンパイルエラーになってしまう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/192a2590-8435-9394-bd78-98421b4bdc5f.png)

そこで`type-fest`の型を独自に拡張して`object`も許可するように型定義を変更する。型定義を以下のコミットのように修正する（コード全体は[ここ](https://github.com/yutak23/type-kebabcase-keys/commit/1415ed3364fa45879175ad344bb5d2642fa09e4e#diff-461d3fa57906650f1c1f2937cbb151dcb685e2e962a6d068548d8312dd7d9f75)を参照）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/73dada9c-f392-5474-c034-963022d0fffd.png)

上記のように修正する事で、Dateなどの任意の`object`をJSONのvalueに渡してもコンパイルエラーにならなくなる。

| 引数 |  |  |
|:-:|:-:|:-:|
| json | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/2c5ac427-3990-a435-ceed-46e57a464443.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/b552cbdb-8923-5538-1084-1c5b45611f56.png) |
| json[] | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/95c04287-5f76-084f-3203-efee9d70dfe3.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/5ed91e35-6504-9fbd-a194-fc9c6992cfd1.png) |

#### 追記

今回はtype-festを参考に、JsonObjectとして型定義を行ったが、`Record<string, unknown>`にする方法の方が良かったかもしれない。理由は、`JsonValue`がユニオン型であるが、これが`object`を含むので[sort-type-constituents](https://typescript-eslint.io/rules/sort-type-constituents)のエラーになるため。

※`Record<string, any>`と`Record<string, unknown>`の違いは、[TypeScriptのRecord<string, any>とRecord<string, unknown>の振る舞い・挙動の違い](https://qiita.com/yuta-katayama-23/items/a26839233167e35c1ae4)を参照。

- 参考：[object、Object、{}の違い](https://typescriptbook.jp/reference/values-types-variables/object/difference-among-object-and-object)
- 参考：[プリミティブ以外はすべてオブジェクト](https://typescriptbook.jp/reference/values-types-variables/object/non-primitives-are-objects)

### Step3

[kebabcase-keys](https://github.com/mattiloh/kebabcase-keys)のJavaScriptコードの実装を見ると分かるが、第二引数に`options`を渡す事ができ、そのoptionsは以下のような型で表現される。

```ts
	type Options = {
		readonly deep?: boolean;
		readonly exclude?: Array<string | RegExp>;
	};
```

それぞれ以下の機能を提供するためのオプションになる。

- `deep`
ネストしたJSONにおいて、第2階層の以下の階層もケースの変換対象にするか？否か？の設定オプション。省略可能。省略した場合は`false`扱い。

- `exclude`
キーの中で、キー or 正規表現 の配列に合致するキーはケース変換の対象から除外する設定オプション。省略可能。省略した場合は`[]`扱い。

これらのオプションに型定義も則るように型定義を修正してみたいと思う。

#### deepオプション

これはネストしたJSONに対してケース変換を適用するか？を設定するものなので、以下のように型定義を修正する事で`deep`がtrueの場合にのみ2階層以下にもケース変換を適用する、を表現できる（コード全体は[ここ](https://github.com/yutak23/type-kebabcase-keys/commit/aa0196f426590ad8b92e9eff9cdb73bb5952a074#diff-461d3fa57906650f1c1f2937cbb151dcb685e2e962a6d068548d8312dd7d9f75)）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/a0088601-039d-2dd3-0e98-7679b0013bc0.png)

実際に`deep: true`の設定有無で、戻りの型定義が変化しているのが確認できる。

| deep: true | deep: false |
|:-:|:-:|
| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/a21d9437-6ad5-6502-59b3-172e52d1c219.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/25ec7646-1760-9148-739d-082fde98c85b.png) |

##### 型定義の方法について

- `options?: OptionsType`
省略可能なoptionsという引数を定義し、その型を`interface Options`に設定している（型引数で`OptionsType extends Options`としているので）。

- `WithDefault<OptionsType['deep'], false>`と`Deep extends boolean`
`WithDefault`は`OptionsType['deep']`がundefinedの場合に、型引数の第2引数の型を返す型。~~つまり、optionsに何も設定しない場合は、boolean型リテラルの`false`として扱われる。~~[別のライブラリのPR](https://github.com/bendrucker/snakecase-keys/pull/115)を作成していて後から気づいたが、`OptionsType['deep']`は`boolean | undefined`または`unkonw`になるので、`WithDefault`の実装はなしで、`Options["deep"] extends boolean ? Options["deep"] : true`とすべきだった（一応、今の実装でも`true`に設定にしない場合は、タプル型`[true]`には合致しないので期待通りの型定義にはなっているが…）。

- `Deep[] extends Array<true> ? A : B`
タイプパラメーターDeepのタプルがtrueのリテラル型のタプルを継承するか？、つまり、Deepタイプパラメーターがboolean型リテラルのtrueであるか？でyesならAを、noならBを、それぞれ型として適用するConditional Typesの実装。これにより、`deep`オプションの有無でネストしたJSONに再帰的に`KebabCasedProperties<T[Key], Deep>`を適用するか？を分岐できる。

#### excludeオプション

ケース変換から除外するキーを列挙するオプション。以下のように型定義を修正する事でexcludeに指定されたオプションに基づいてケース変換の適用有無を切り替える、を表現できる（コード全体は[ここ](https://github.com/yutak23/type-kebabcase-keys/commit/0f02b762c793476a0adeb13f051eea4f3c292873)）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/f331af9f-c60c-d238-740b-30c6fe327d32.png)

実際に`exclude: ['fooBar'] as const`の設定有無で、戻りの型定義が変化しているのが確認できる。

| exclude未指定 | exclude: ['fooBar'] as const |
|:-:|:-:|
| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/b812cb20-250f-bf49-7a1a-fe3ec1f23a77.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/a882a6c4-7451-9573-6e88-c2cd264265a1.png) |

※ただし、正規表現の場合は上記のような挙動にはならないので、`(result as any).hoge`のような実装にならざる負えないだろう。

##### 型定義の方法について

- `WithDefault<OptionsType['exclude'], []>`
deepの時と同じで、OptionsType型からexcludeプロパティの型を取得し（`as const`によりリテラルなタプル型になるが）、それがundefinedであればデフォルト値としてEmptyTuple（`[]`）にするという実装。

- `Array<Includes<Exclude, Key>> extends Array<true> ? Key : KebabCase<Key>`
`Includes`の実装はtype-festの[Includes](https://github.com/sindresorhus/type-fest/blob/main/source/includes.d.ts)そのもので、`Includes`で利用している`IsEqual`はタプル型を利用して、AとBが一致するか？を判定し、boolean型リテラルのtrue or falseにしている。つまり、excludeで渡される配列リテラル（[array literal](https://typescriptbook.jp/reference/values-types-variables/array/array-literal)）のタプル型の各要素がTのJSONのキーと一致するか？を判定。それが一致する=trueになれば`Array<Includes<Exclude, Key>>`が`Array<true>`を継承している事になるので、三項演算子の`Key`の型が適用されexcludeに列挙されたキーはkebab-caseにならない、が実現できる。

- `type IsEqual<A, B> = A[] extends B[] ? (B[] extends A[] ? true : false) : false;`
タプル型を駆使してAとBが同じか？を判定している、タプル型のA（[A]）タプル型のB（[B]）がそれぞれ互いにextendsで継承しているという事は、部分集合とかではなく完全に一致している事が保障されるので、それで同じか？を判定している。

- `{ exclude: ['fooBar'] as const }`について
まず、`OptionsType['exclude']`の意味だが、これはOptionsType型からexcludeプロパティの**型**を取得する記法。そのため、`exclude: ['fooBar']`の場合は、`OptionsType['exclude']`の型は`string[]`になる（以下の画像の通り）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/660a8c2b-d22f-10ec-0e89-2a9c745a1106.png)
`deep`オプションの場合は、true/falseがboolean型リテラルなのでそのままリテラルな型として渡されるので、引数の値がそのまま渡っているかの感覚になるが、あくまで`OptionsType['deep']`はdeepプロパティの「型」を取得しているに過ぎない。
が、[const assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions)を利用すると、`exclude: ['fooBar'] as const`は以下のような型（readonlyのタプル型）になる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/d8c43430-14df-0428-6476-1736b981df5a.png)
この`as const`（const assertion）が何者か？だが、リテラル型であればそのリテラル型を拡張しない（たとえば、"hello"から"string"に変更しない）といったアサーション機能で、今回のように配列リテラルの場合は読み取り専用のタプルになる。すなわち、型定義の中で扱われる時にOptionsType型からexcludeプロパティの型を取得する際に、['fooBar']は`string[]`ではなく、タプル型`['fooBar']`として型取得されるので、タプルの要素に除外したいキー名が含まれるので、それを上記の`Includes<Exclude, Key>`で利用できるようになる。

・参考：[How do I declare a read-only array tuple in TypeScript?](https://stackoverflow.com/questions/42696098/how-do-i-declare-a-read-only-array-tuple-in-typescript)

> With const assertions, the compiler can be told to treat an array or an object as immutable, meaning that their properties are read-only. This also allows the creation of literal tuple types with narrower type inference (i.e. your ["a", "b"] can be of type ["a", "b"], rather than string[] without specifiying the whole thing as a contextual type)

## PRを上げる

型定義をローカルの環境で作成で来たら、いよいよDefinitelyTypedにPRを作成する。全体の流れは[How can I contribute?](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master#how-can-i-contribute)に書かれている。

流れとしては以下。

1. PRを作成する前に、自分のローカル環境で型定義を行い利用できるか？チェックする
→上記の手順でdone
1. （今回は新規に型定義を作成するので）[Create a new package](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master#create-a-new-package)にある方法でひな形を作成する
1. 自動で作成されたテンプレートの`index.d.ts`を、1の手順で作成した型定義で更新する
1. prettierとdtslintを繰り返し、エラーがなくなるまで調整する
1. `npm run test-all`でエラーが出ない事を確認
1. PR作成、レビュー指摘を修正してマージされたら完了

以下で詳細を見ていく。

### 自動で作成されたテンプレートの`index.d.ts`を、1の手順で作成した型定義で更新する

以下のコマンドで型定義のひな型を作成できる。

```console
$ npx dts-gen --dt --name kebabcase-keys --template module
```

あとは、上記の[型定義を行う](#型定義を行う)で作成した型定義をコピペすればいい。

### prettierとdtslintを繰り返し、エラーがなくなるまで調整する

Prettierは[公式](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master#common-mistakes)に書かれている通りのコマンドを実行すればいい。

```console
$ npm run prettier -- --write types/kebabcase-keys/**/*.ts
```

dtslintについては、以下のように型定義のテストを書いていけばいい。

```ts:types/kebabcase-keys/kebabcase-keys-tests.ts
import kebabcaseKeys = require('kebabcase-keys');

kebabcaseKeys({}); // $ExpectType {}
kebabcaseKeys({ foo_bar: true }); // $ExpectType { "foo-bar": true; }

kebabcaseKeys([]); // $ExpectType never[]
```

テストを実装したら、以下のコマンドでテストを実行しエラーが出なければOK。テストと言っても型を確認しているだけでJestやVitestのようなものではない。型定義が問題ない事を確認するためのものなので、できるだけ多くのパターンで型チェックのテストを書くのが良いみたい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/48bd77fc-8cee-4e8a-05c1-b03357f6ed16.png)

例えば型定義の期待値がずれていれば、以下のようにdtslintの方でエラーになる。

| エラーになるテスト | エラーの内容 |
|:-:|:-:|
| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/1ce5a55f-172a-0165-7339-0dc4e52fcd90.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/2b6bc965-3845-c6ab-90a2-8d1287383607.png) |

### `npm run test-all`でエラーが出ない事を確認

PRを作成すると、CIで`npm run test-all`が実行されるので、ローカル環境でも同じテストを実行し、エラーがない事を確認する。

### PR作成、レビュー指摘を修正してマージされたら完了

最後に、PRを作成してレビューの指摘があればそれを修正し、マージされれば完了になる。私のPRは2つあったが、それぞれ比較的直にレビューをしてもらえたので、PR作成から1～2日でマージとなった。

https://github.com/DefinitelyTyped/DefinitelyTyped/pull/65820

https://github.com/DefinitelyTyped/DefinitelyTyped/pull/65846