2019/12 版TypeScriptおすすめ設定

本記事は [NewsPicks Advent Calendar 2019](https://qiita.com/advent-calendar/2019/newspicks) 9日目の記事です。

* 2019/12/20: VSCode 1.41.1 のリリースに伴い `editor.codeActionsOnSave` という設定項目ができたので .vscode/settings.json を更新しました。

最近TypeScriptを書く機会が多いので、備忘録としておすすめ設定をまとめておきます。
ついでに、標準では存在しないけどあったら便利だなというメソッドをメモしておきます。

# 2019/12 版 TypeScript おすすめ設定

## ざっくりとした内容

* nodenvでnodeのバージョン指定
* eslintでlint+フォーマット
* .vscode連携

ファイルがいっぱいあって大変ですが、、しょうがないのでやりましょう。
基本的には下記の設定をした上で、プロジェクトごとに必要な設定を足す使い方を想定しています。

## .node-version

任意のnodeのバージョンを書きます。nodenvを利用していると、このプロジェクト内でのnodeのバージョンがこのバージョンに固定されます。

```
12.13.1
```

## npm (package.json)

eslint関連パッケージをインストールしておきます。

```bash
npm i -D \
 @typescript-eslint/eslint-plugin \
 @typescript-eslint/parser \
 @typescript-eslint/typescript-estree \
 eslint \
 eslint-config-prettier \
 eslint-plugin-prettier \
 eslint-plugin-simple-import-sort \
 prettier
```

package.jsonの`"scripts"`に下記を追加します。

```json
    "lint": "eslint . --ext .ts --fix"
```

`npm run lint` で eslintが起動できます。


## .eslintrc

ひたすらおすすめ設定を入れるだけです。こういうところで出来るだけ工数を取られたくないですね。
importのソートはいくつか方法があるのですが、一番設定が簡単なものを採用しています。

```json
{
    "parser": "@typescript-eslint/parser",
    "extends": [
        "eslint:recommended",
        "plugin:@typescript-eslint/eslint-recommended",
        "plugin:@typescript-eslint/recommended",
        "plugin:prettier/recommended",
        "prettier/@typescript-eslint"
    ],
    "plugins": [
        "@typescript-eslint",
        "prettier",
        "simple-import-sort"
    ],
    "rules": {
        "@typescript-eslint/explicit-function-return-type": ["warn", { "allowExpressions": true }],
        "simple-import-sort/sort": "error"
    }
}
```

## .eslintignore

typescriptが出力する型定義ファイルをeslintが見ないようにします。

```
*.d.ts
```

## .prettierrc

自動フォーマットでのインデント幅を調整します。

```json
{
    "tabWidth": 4
}
```

## .editorconfig

ファイル末尾の改行がつくようにしておきます。typescriptのファイル以外も有効です。

```ini
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
```

## .tsconfig.json

`"compilerOptions"`の`"lib"`に`"es2019.array"`が入ってなかったら入れておきます。`flatMap()`が有効になります。

## .vscode/settings.json

vscodeではプロジェクトごとの設定と個人用設定を別に持てるので、プロジェクト単位で統一したい設定はリポジトリにコミットしてしまっていいんじゃないかと思っています。というわけで以下のようにします。ファイルの保存時にeslintに統合されたprettierが発動し、コードを自動でフォーマットします。

```json
{
  "typescript.tsdk": "./node_modules/typescript/lib",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}
```

# 標準では存在しないけどあると便利なメソッド

そのうちnpm化します。きっと、、、

```typescript
/**
 * タプルを返します
 * @param args
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function tuple<T extends any[]>(...args: T): T {
    return args;
}

export type Func<T, R> = (t: T) => R;

export type KeyValuesPair<K, V> = [K, V[]];

export function isTruthy<T>(x: T | undefined): x is T {
    return Boolean(x);
}

declare global {
    interface Array<T> {
        filterIn(this: T[], others: Iterable<T>): Array<T>;
        filterNotIn(this: T[], others: Iterable<T>): Array<T>;

        groupBy<K>(this: T[], keySelector: Func<T, K>): KeyValuesPair<K, T>[];
        groupBy<K, V>(
            this: T[],
            keySelector: Func<T, K>,
            valueSelector: Func<T, V>
        ): KeyValuesPair<K, V>[];

        toMapBy<K>(this: T[], keySelector: Func<T, K>): Map<K, T>;
        toMapBy<K, V>(
            this: T[],
            keySelector: Func<T, K>,
            valueSelector: Func<T, V>
        ): Map<K, V>;

        toMap<K, V>(this: [K, V][]): Map<K, V>;

        unique(this: T[]): T[];

        compact(this: T[]): NonNullable<T>[];
    }

    interface Set<T> {
        /**
         * returns union set
         * @param others
         */
        add(this: Set<T>, others: Set<T>): Set<T>;
        /**
         * returns intersection set
         * @param others
         */
        intersect(this: Set<T>, others: Set<T>): Set<T>;
        /**
         * returns difference set
         * @param others
         */
        subtract(this: Set<T>, others: Set<T>): Set<T>;
    }
}

Array.prototype.filterIn = function<T>(
    this: T[],
    others: Iterable<T>
): Array<T> {
    const set = others instanceof Set ? (others as Set<T>) : new Set(others);
    return this.filter(x => set.has(x));
};

Array.prototype.filterNotIn = function<T>(
    this: T[],
    others: Iterable<T>
): Array<T> {
    const set = others instanceof Set ? (others as Set<T>) : new Set(others);
    return this.filter(x => !set.has(x));
};

function groupBy<T, K>(
    this: T[],
    keySelector: Func<T, K>
): KeyValuesPair<K, T>[];
function groupBy<T, K, V>(
    this: T[],
    keySelector: Func<T, K>,
    valueSelector: Func<T, V>
): KeyValuesPair<K, V>[];
function groupBy<T, K, V>(
    this: T[],
    keySelector: Func<T, K>,
    valueSelector?: Func<T, V>
): KeyValuesPair<K, V | T>[] {
    const map = new Map<K, (V | T)[]>();
    const keys = [] as K[];
    this.forEach(x => {
        const key = keySelector(x); // to let result be stable order
        const collection = map.get(key);
        if (!collection) {
            map.set(key, [valueSelector ? valueSelector(x) : x]);
            keys.push(key);
        } else {
            collection.push(valueSelector ? valueSelector(x) : x);
        }
    });
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    return keys.map(key => tuple(key, map.get(key)!));
}

Array.prototype.groupBy = groupBy;

function toMapBy<T, K>(this: T[], keySelector: Func<T, K>): Map<K, T>;
function toMapBy<T, K, V>(
    this: T[],
    keySelector: Func<T, K>,
    valueSelector: Func<T, V>
): Map<K, V>;
function toMapBy<T, K, V>(
    this: T[],
    keySelector: Func<T, K>,
    valueSelector?: Func<T, V>
): Map<K, V | T> {
    return new Map(
        this.map(x =>
            tuple(keySelector(x), valueSelector ? valueSelector(x) : x)
        )
    );
}

Array.prototype.toMapBy = toMapBy;

Array.prototype.toMap = function<K, V>(this: [K, V][]): Map<K, V> {
    return new Map(this);
};

Array.prototype.unique = function<T>(this: T[]): T[] {
    const set = new Set<T>();
    const result = [] as T[];
    for (const x of this) {
        if (set.has(x)) {
            continue;
        }
        set.add(x);
        result.push(x);
    }
    return result;
};

Array.prototype.compact = function<T>(this: (T | undefined)[]): T[] {
    return this.filter(isTruthy);
};

Set.prototype.add = function<T>(this: Set<T>, others: Set<T>): Set<T> {
    return new Set([...this, ...others]);
};

Set.prototype.intersect = function<T>(this: Set<T>, others: Set<T>): Set<T> {
    return new Set(Array.from(this).filterIn(others));
};

Set.prototype.subtract = function<T>(this: Set<T>, others: Set<T>): Set<T> {
    return new Set(Array.from(this).filterNotIn(others));
};
```

# 終わり

明日は @kz_morita さんの「Github Actions で FizzBuzzしてみる」です。お楽しみに。