TypeScriptプロジェクトでstrict:trueに切り替えた時に出たエラーと対応まとめ


# ざっくりプロジェクト情報

### TypeScript

- version: 3.5.3

### 主なライブラリ

- React
- React-Router
- StyledComponent
- NestJs
- TypeORM
- class-validator
- class-transformer

### tsconfig.json

Before:

```json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": false,
    "noImplicitReturns": true,
    "noImplicitThis": false,
    "noImplicitUseStrict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": false,
    "pretty": true,
    "sourceMap": true,
    "inlineSources": true,
    "strictNullChecks": true,
    "resolveJsonModule": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
```

After:

```json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "strict": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "pretty": true,
    "sourceMap": true,
    "inlineSources": true,
    "resolveJsonModule": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

```

有効になるもの:

- `noImplicitAny`
- `noImplicitThis`
- `strictBindCallApply`
- `strictFunctionTypes`
- `strictPropertyInitialization`
- `noUnusedParameters`

（タイトルに反して `strict` の他に `noUnusedParameters` もtrueにしてます）

※基本的にPR、修正量が巨大になるので、一気に `strict:true` にするのではなく、個々の設定値で分けて段階的に厳しくしていくこともおすすめです


### 直後の総エラー数

約1,000件

かなり少ない方、という感覚

ここから以下、出てきたエラーとその対応

# noUnusedParameters

`'変数名' is declared but its value is never read.`

エラー数は大量だけど修正の仕方はあまり悩まないケースが多い

### 基本形（本当に不要）

対応：削除する。以上

### 削除できない / したくない

対応： `_` プレフィックスをつけて利用しないことを明示する


```ts
// resにアクセスするけどreqは使わない
router.get("/", (req, res) => { })
```

↓

```ts
router.get("/", (_req, res) => { })
```

# `strictPropertyInitialization`

`Property 'プロパティ名' has no initializer and is not definitely assigned in the constructor.`

### 基本形（本当に型が間違っている）

本当に初期化されないので `undefined` になるのに、型がそうなっていない

対応：正しくnullableにする

```ts
class A {
  hoge: string;
}
```

↓

```ts
class A {
  hoge: string | undefined;
}
```

### 初期化していなくて実はundefinedで動いていたが実質それで良い

boolean型で初期値をとしてfalseを期待しているときなど、初期化してなくてもほぼ健在化しない

対応：初期値をセット

```ts
class A {
  hoge: boolean;
}
```

```ts
class A {
  hoge: boolean = false
}
```

良くない対応：必要ないのに、エラーを消すためにnullableにしてしまう

```ts
class A {
  hoge: boolean | undefined
}
```
### class-transfomerクラス

インスタンスの生成はclass-transfomerで行うので、コンストラクタは書きたくない

対応： `!` をつけてしまう。

```ts
class A {
  @Expose() readonly hoge: string;
}
```

↓

```ts
class A {
  @Expose() readonly hoge!: string;
}
```

（TODO: privateのダミーコンストラクタを定義して、意図しないnewでの生成が出来ないようにできる気がするので、検証して追記）

### TypeORMのEntityクラスで `| null` が足りなかった

対応： 型や初期値を修正しつつ、このとき **DB側の型が自動で識別されなくなるのでColumn()のパラメータも修正が必要**

```ts
  @Column({
    nullable: true
  })
  @Type(() => Date)
  acceptedAt: Date;
```

```ts
  @Column("datetime", {
    nullable: true
  })
  @Type(() => Date)
  acceptedAt: Date | null = null;
```

プリミティブな型でないと、TypeORMが自動で識別できない（ https://github.com/typeorm/typeorm/issues/1358 ）

### TypeORMのEntityクラスで、DBがセットするフィールドのno initializerエラー

auto incrementやcurrent timestampのフィールドは、初期化しようがない

対応： `!` をつけてしまう

```ts
  @PrimaryGeneratedColumn()
  id: number;
```

↓

```ts
  @PrimaryGeneratedColumn()
  id!: number;
```

別解１： `| null` を付ける
別解２： 未保存時用に別のinterfaceを定義する

このプロジェクトでは `new` でエンティティを初期化して直後`save`するケースがほとんどなので、 `!` 許容と判断しました

# `noImplicitAny`

## ImplicitAnyIndexErrors 以外

`Parameter 'request' implicitly has an 'any' type.`

### 基本形（単純に型アノテーションのつけわすれ）

対応：アノテーション付ける。以上

```ts
function func(hoge) {
}
```

```ts
function func(hoge: string) {
}
```

### any型のオブジェクトに `.map()` などを呼び出したとき

対応：ゆるい型でもよいのでanyを回避する

```ts
    // resultsはany型
    const results = await connection.query(query, [...]);

    // r が怒られる
    const convertedResults = results.map(r => {
    });
```

```ts
    const results: { [key: string]: unknown}[] = await connection.query(query, [...]);

    const convertedResults = results.map(r => {
    });
```

## 型定義がインストールされていない

### 基本形（単純にインストールし忘れ）

対応：インストールする。以上

```
`TS7016: Could not find a declaration file for module '@storybook/addon-actions'. 'パス〜/index.js' implicitly has an 'any' type.
```

```
yarn add -D @types/storybook__addon-actions
```

２つくらいインストールが漏れて暗黙的にanyで動いていた…

### 型定義が提供されていないライブラリ

対応：握りつぶしてしまう

```ts
import * as xlsxPopulate from "xlsx-populate";
```

```ts
// @ts-ignore: Could not find a declaration file for module
import * as xlsxPopulate from "xlsx-populate";
```

別解：自分で型情報を定義する

このプロジェクトのケースでは、そのライブラリを呼び出し箇所がごく少なく、複雑でもないので握りつぶしてOKと判断しました

anyのままは危険過ぎる場合や、モチベーションが湧く場合はもちろん型定義を書く判断はもちろんアリですが、
**strict:true に変更してそのエラーを修正するブランチ・PRとは分けて対応したほうが良いと思います**


### 最新の型ファイルだと使えない

`query-string` で遭遇。

```
❯ yarn add -D @types/query-string
warning @types/query-string@6.3.0: This is a stub types definition. query-string provides its own type definitions, so you do not need this installed.
```

最新バージョンでは、ライブラリ本体に型定義が含まれるようになっていて、 `@types/query-string` はダミーになっている。なのでインストールしても意味がない

そのうえで

> This module targets Node.js 6 or later and the latest version of Chrome, Firefox, and Safari. If you want support for older browsers, or, if your project is using create-react-app v1, use version 5: npm install query-string@5.

old browsersサポートのためプロジェクトで5.xを使っている場合、ライブラリ本体にもインストールした `@types/query-string` にも型情報が無いという状態になる

対応： typesもバージョンを指定してインストール

```
yarn add -D @types/query-string@5
```

## ImplicitAnyIndexErrors

ブラケット記法でアクセスしているときに遭遇するエラー

### 基本形

対応：index typeを書く

```ts
const hash = {
  a: 1,
  b: 2,
  c: 3
};


hash[key]
```

↓

```ts
const hash: { [key: string]: number } = {
  a: 1,
  b: 2,
  c: 3
};
```


### `{}` 型のimplicit any index errors

対応： 初期値が `{}` なときは型を明示

```ts
    // results: { key: string; count: number }[]

    const resultMap = {};
    for (const r of results) {
      resultMap[r.key] = r.count;
    }
```

```ts
    const resultMap: { [key: string]: number } = {};
    for (const r of results) {
      resultMap[r.time] = r.count; // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type '{}'.
    }
```

### ジェネリクス

対応： index typeを継承していることを明示する

```
error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'unknown'.
  No index signature with a parameter of type 'string' was found on type 'unknown'.

32       const value = obj[key];
```

```ts
  func<T>(key, obj: T): T {
    const value = obj[key];
  }
```

↓

```ts
  func<T extends { [key: string]: unknown >(key, obj: T): T {
    const value = obj[key];
  }
```

### indexアクセスを回避できるとき

例その１

対応： 配列にできそう

```ts
    const names = {
      1: "山田 太郎",
      2: "鈴木 一郎"
    };

    for (const i of Object.keys(names)) {
      console.log(`[User${i}] name: ${names[i]}`);
    }
```


```ts
    const names = [
      "山田 太郎",
      "鈴木 一郎"
    ];

    for (const [i, name] of names.entries()) {
      console.log(`[User${i + 1}] name: ${name}`);
    }

```

例その２

対応： `Object.values()` や `Object.entries()` が使えそう

```ts
    const clientErrors = Object.keys(ClientErrorCode).map(
      k => ClientErrorCode[k]
    );
```

```ts
    const clientErrors = Object.values(ClientErrorCode);
```


### 握りつぶす

ライブラリの型定義で手が出しづらいときなど

ブラケット記法で変数アクセスしているときは頑張っても型で安全性を高めるのが難しいことが多い気がするので、諦めも必要と思う

具体例： class-validatorでカスタムバリデーションを作成
https://github.com/typestack/class-validator#custom-validation-decorators

```ts
export function IsGreaterThan(
  property: string,
  validationOptions?: ValidationOptions
) {
  return (object: Object, propertyName: string) => {
    registerDecorator({
      name: "IsGreaterThan",
      target: object.constructor,
      propertyName: propertyName,
      constraints: [property],
      options: validationOptions,
      validator: {
        validate(value: any, args: ValidationArguments) {
          const [relatedPropertyName] = args.constraints;
          const relatedValue = args.object[relatedPropertyName]; // Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'Object'.

          return value > relatedValue;
        }
      }
    });
  };
}
```

↓

```ts
          const relatedValue = (<any>args.object)[relatedPropertyName];
```



# strictFunctionTypes

### 基本形（本当に型が一致していない）

例： Uint8Array と number[]

対応：適切に変換する

```
TS2345: Argument of type 'Uint8Array' is not assignable to parameter of type 'number[]'.
  Type 'Uint8Array' is missing the following properties from type 'number[]': pop, push, concat, shift, and 3 more.
```

```ts
String.fromCharCode.apply(
        null,
        new Uint8Array(data.slice(start, end))
      );
```

↓

```ts
String.fromCharCode.apply(
        null,
        Array.from(new Uint8Array(data.slice(start, end)))
      );
```

### nullable引数の罠

strictFunctionTypesを有効にすると、「関数引数の引数」のNullable/NonNullableの条件は混乱しがち（「普通の変数」のassignのときと逆転する）

Reactのイベントハンドラー周りで遭遇しがちと思う

```
TS2322: Type '(_e: MouseEvent<HTMLElement, MouseEvent>) => Promise<void>' is not assignable to type 'event?: MouseEvent<HTMLElement, MouseEvent>'.
    Types of parameters '_e' and 'event' are incompatible.
      Type 'MouseEvent<HTMLElement, MouseEvent> | undefined' is not assignable to type 'MouseEvent<HTMLElement, MouseEvent>'.
        Type 'undefined' is not assignable to type 'MouseEvent<HTMLElement, MouseEvent>'.
```

```ts
  public render() {
    // Componentの props.handler: (event?: MouseEvent<HTMLElement, MouseEvent>)
    return <Component handler={this.handler} />;
  }

  private handler = (e: MouseEvent<HTMLElement, MouseEvent>) => {
  };
```

対応： `MouseEvent` の省略可・不可 (`e:` か `e?:` か）を正しく合わせる
↓

```ts
  private handler = (e?: MouseEvent<HTMLElement, MouseEvent>) => {
  };
```


### withRouter() のときの正しいprops

```
TS2345: Argument of type 'typeof Component' is not assignable to parameter of type 'ComponentType<RouteComponentProps<any, StaticContext, any>>'.
  Type 'typeof Component' is not assignable to type 'ComponentClass<RouteComponentProps<any, StaticContext, any>, any>'.
    Type 'Component' is not assignable to type 'Component<RouteComponentProps<any, StaticContext, any>, any, any>'.
      Types of property 'props' are incompatible.
        Type 'Readonly<IProps> & Readonly<{ children?: ReactNode; }>' is not assignable to type 'Readonly<RouteComponentProps<any, StaticContext, any>> & Readonly<{ children?: ReactNode; }>'.
          Property 'location' is missing in type 'Readonly<IProps> & Readonly<{ children?: ReactNode; }>' but required in type 'Readonly<RouteComponentProps<any, StaticContext, any>>'.
```

```ts
import { withRouter } from "react-router";

type Props = {
  history: History;
};

class Component extends React.Component<Props, State> {
}

export default withRouter(Component);
```

対応： `RouteComponentProps` が用意してあるので使う
↓

```ts
import { RouteComponentProps, withRouter } from "react-router";

type Props = RouteComponentProps;
```

# さいごに

null assertionやts-ignoreなどを完全に排除するよう頑張るよりも、多少は受け入れるくらいの柔軟さは有ってよいのでは派です

`noImplicitAny: false`のままにしたり、 `noImplicitAny: true` でも `suppressImplicitAnyIndexErrors: true` にするなど、無理せず柔軟に、チームが快適な型安全性レベルを選べばよいと思います

---

宣伝

この記事の内容は、[株式会社EventHub](https://eventhub.jp/)のプロジェクトでの実例に基づきます。

[株式会社EventHub](https://eventhub.jp/) では「イベントでの繋がりを増やす」をテーマに、参加者同士の情報交換・面談を促進するイベント・マーケティングツールを開発しています。

エンジニア積極採用中ですのでご興味がある方は [support@eventhub.jp](mailto:support@eventhub.jp) まで連絡お願いします。